tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
   8
   9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  11
  12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH
  13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  18"""
  19
  20# Copyright (c) 2022 Gilmillin Timur Mansurovich
  21#
  22# Licensed under the Apache License, Version 2.0 (the "License");
  23# you may not use this file except in compliance with the License.
  24# You may obtain a copy of the License at
  25#
  26#     http://www.apache.org/licenses/LICENSE-2.0
  27#
  28# Unless required by applicable law or agreed to in writing, software
  29# distributed under the License is distributed on an "AS IS" BASIS,
  30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31# See the License for the specific language governing permissions and
  32# limitations under the License.
  33
  34
  35import sys
  36import os
  37from argparse import ArgumentParser
  38from importlib.metadata import version
  39
  40from dateutil.tz import tzlocal
  41from time import sleep
  42
  43import re
  44import json
  45import requests
  46import traceback as tb
  47from typing import Union
  48
  49from multiprocessing import cpu_count
  50from multiprocessing.pool import ThreadPool
  51import pandas as pd
  52
  53from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  54from TradeRoutines import *  # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module
  55
  56from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator
  57from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  58
  59import UniLogger as uLog  # Logger for TKSBrokerAPI
  60
  61
  62# --- Common technical parameters:
  63
  64PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  65uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  66uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  67uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  68
  69__version__ = "1.5"  # The "major.minor" version setup here, but build number define at the build-server only
  70
  71CPU_COUNT = cpu_count()  # host's real CPU count
  72CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  73
  74
  75class TinkoffBrokerServer:
  76    """
  77    This class implements methods to work with Tinkoff broker server.
  78
  79    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  80
  81    About `token`: https://tinkoff.github.io/investAPI/token/
  82    """
  83    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  84        """
  85        Main class init.
  86
  87        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  88        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  89                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  90        :param useCache: use default cache file with raw data to use instead of `iList`.
  91                         True by default. Cache is auto-update if new day has come.
  92                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  93        :param defaultCache: path to default cache file. `dump.json` by default.
  94        """
  95        if token is None or not token:
  96            try:
  97                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
  98                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
  99
 100            except KeyError:
 101                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 102                raise Exception("Token required")
 103
 104        else:
 105            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 106            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 107
 108        if accountId is None or not accountId:
 109            try:
 110                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 111                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 112
 113            except KeyError:
 114                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 115
 116        else:
 117            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 118            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 119
 120        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 121        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 122
 123        Latest version: https://pypi.org/project/tksbrokerapi/
 124        """
 125
 126        self.aliases = TKS_TICKER_ALIASES
 127        """Some aliases instead official tickers.
 128
 129        See also: `TKSEnums.TKS_TICKER_ALIASES`
 130        """
 131
 132        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 133
 134        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 135
 136        self._ticker = ""
 137        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 138
 139        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 140        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 141
 142        See also: `SearchByTicker()`, `SearchInstruments()`.
 143        """
 144
 145        self._figi = ""
 146        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 147
 148        See also: `SearchByFIGI()`, `SearchInstruments()`.
 149        """
 150
 151        self.depth = 1
 152        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 153
 154        See also: `GetCurrentPrices()`.
 155        """
 156
 157        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 158        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 159
 160        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 161        """
 162
 163        uLogger.debug("Broker API server: {}".format(self.server))
 164
 165        self.timeout = 15
 166        """Server operations timeout in seconds. Default: `15`.
 167
 168        See also: `SendAPIRequest()`.
 169        """
 170
 171        self.headers = {
 172            "Content-Type": "application/json",
 173            "accept": "application/json",
 174            "Authorization": "Bearer {}".format(self.token),
 175            "x-app-name": "Tim55667757.TKSBrokerAPI",
 176        }
 177        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 178
 179        See also: `SendAPIRequest()`.
 180        """
 181
 182        self.body = None
 183        """Request body which send to broker server. Default: `None`.
 184
 185        See also: `SendAPIRequest()`.
 186        """
 187
 188        self.moreDebug = False
 189        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 190
 191        self.historyFile = None
 192        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 193
 194        See also: `History()`.
 195        """
 196
 197        self.htmlHistoryFile = "index.html"
 198        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 199
 200        See also: `ShowHistoryChart()`.
 201        """
 202
 203        self.instrumentsFile = "instruments.md"
 204        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 205
 206        See also: `ShowInstrumentsInfo()`.
 207        """
 208
 209        self.searchResultsFile = "search-results.md"
 210        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 211
 212        See also: `SearchInstruments()`.
 213        """
 214
 215        self.pricesFile = "prices.md"
 216        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 217
 218        See also: `GetListOfPrices()`.
 219        """
 220
 221        self.infoFile = "info.md"
 222        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 223
 224        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 225        """
 226
 227        self.bondsXLSXFile = "ext-bonds.xlsx"
 228        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 229        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 230
 231        See also: `ExtendBondsData()`.
 232        """
 233
 234        self.calendarFile = "calendar.md"
 235        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 236        
 237        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 238
 239        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 240        """
 241
 242        self.overviewFile = "overview.md"
 243        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 244
 245        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 246        """
 247
 248        self.overviewDigestFile = "overview-digest.md"
 249        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 250
 251        See also: `Overview()` with parameter `details="digest"`.
 252        """
 253
 254        self.overviewPositionsFile = "overview-positions.md"
 255        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 256
 257        See also: `Overview()` with parameter `details="positions"`.
 258        """
 259
 260        self.overviewOrdersFile = "overview-orders.md"
 261        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 262
 263        See also: `Overview()` with parameter `details="orders"`.
 264        """
 265
 266        self.overviewAnalyticsFile = "overview-analytics.md"
 267        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 268
 269        See also: `Overview()` with parameter `details="analytics"`.
 270        """
 271
 272        self.overviewBondsCalendarFile = "overview-calendar.md"
 273        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 274
 275        See also: `Overview()` with parameter `details="calendar"`.
 276        """
 277
 278        self.reportFile = "deals.md"
 279        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 280
 281        See also: `Deals()`.
 282        """
 283
 284        self.withdrawalLimitsFile = "limits.md"
 285        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 286
 287        See also: `OverviewLimits()` and `RequestLimits()`.
 288        """
 289
 290        self.userInfoFile = "user-info.md"
 291        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 292
 293        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 294        """
 295
 296        self.userAccountsFile = "accounts.md"
 297        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 298
 299        See also: `OverviewAccounts()`, `RequestAccounts()`.
 300        """
 301
 302        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 303        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 304
 305        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 306
 307        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 308        """
 309
 310        self.iList = None  # init iList for raw instruments data
 311        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 312        
 313        See also: `Listing()`, `DumpInstruments()`.
 314        """
 315
 316        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 317        if useCache:
 318            if os.path.exists(self.iListDumpFile):
 319                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 320                curTime = datetime.now(tzutc())
 321
 322                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 323                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 324
 325                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 326
 327                else:
 328                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 329
 330                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 331                        os.path.abspath(self.iListDumpFile),
 332                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 333                    ))
 334
 335            else:
 336                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 337                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 338
 339        else:
 340            self.iList = self.Listing()  # request new raw instruments data from broker server
 341            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 342
 343        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 344        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 345
 346        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 347        """
 348
 349    @property
 350    def ticker(self) -> str:
 351        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 352
 353        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 354        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 355
 356        See also: `SearchByTicker()`, `SearchInstruments()`.
 357        """
 358        return self._ticker
 359
 360    @ticker.setter
 361    def ticker(self, value):
 362        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 363
 364        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 365        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 366
 367        See also: `SearchByTicker()`, `SearchInstruments()`.
 368        """
 369        self._ticker = str(value).upper()  # Tickers may be upper case only
 370
 371    @property
 372    def figi(self) -> str:
 373        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 374
 375        See also: `SearchByFIGI()`, `SearchInstruments()`.
 376        """
 377        return self._figi
 378
 379    @figi.setter
 380    def figi(self, value):
 381        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 382
 383        See also: `SearchByFIGI()`, `SearchInstruments()`.
 384        """
 385        self._figi = str(value).upper()  # FIGI may be upper case only
 386
 387    def _ParseJSON(self, rawData="{}") -> dict:
 388        """
 389        Parse JSON from response string.
 390
 391        :param rawData: this is a string with JSON-formatted text.
 392        :return: JSON (dictionary), parsed from server response string.
 393        """
 394        responseJSON = json.loads(rawData) if rawData else {}
 395
 396        if self.moreDebug:
 397            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 398
 399        return responseJSON
 400
 401    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 402        """
 403        Send GET or POST request to broker server and receive JSON object.
 404
 405        self.header: must be defining with dictionary of headers.
 406        self.body: if define then used as request body. None by default.
 407        self.timeout: global request timeout, 15 seconds by default.
 408        :param url: url with REST request.
 409        :param reqType: send "GET" or "POST" request. "GET" by default.
 410        :param retry: how many times retry after first request if an 5xx server errors occurred.
 411        :param pause: sleep time in seconds between retries.
 412        :return: response JSON (dictionary) from broker.
 413        """
 414        if reqType.upper() not in ("GET", "POST"):
 415            uLogger.error("You can define request type: `GET` or `POST`!")
 416            raise Exception("Incorrect value")
 417
 418        if self.moreDebug:
 419            uLogger.debug("Request parameters:")
 420            uLogger.debug("    - REST API URL: {}".format(url))
 421            uLogger.debug("    - request type: {}".format(reqType))
 422            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 423            uLogger.debug("    - body:\n{}".format(self.body))
 424
 425        # fast hack to avoid all operations with some tickers/FIGI
 426        responseJSON = {}
 427        oK = True
 428        for item in self.exclude:
 429            if item in url:
 430                if self.moreDebug:
 431                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 432
 433                oK = False
 434                break
 435
 436        if oK:
 437            counter = 0
 438            response = None
 439            errMsg = ""
 440
 441            while not response and counter <= retry:
 442                if reqType == "GET":
 443                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 444
 445                if reqType == "POST":
 446                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 447
 448                if self.moreDebug:
 449                    uLogger.debug("Response:")
 450                    uLogger.debug("    - status code: {}".format(response.status_code))
 451                    uLogger.debug("    - reason: {}".format(response.reason))
 452                    uLogger.debug("    - body length: {}".format(len(response.text)))
 453                    uLogger.debug("    - headers:\n{}".format(response.headers))
 454
 455                # Server returns some headers:
 456                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 457                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 458                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 459                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 460                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 461                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 462                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 463                    sleep(rateLimitWait)
 464
 465                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 466                if 400 <= response.status_code < 500:
 467                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 468                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 469
 470                    if "code" in response.text and "message" in response.text:
 471                        msgDict = self._ParseJSON(rawData=response.text)
 472                        uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 473
 474                    counter = retry + 1  # do not retry for 4xx errors
 475
 476                if 500 <= response.status_code < 600:
 477                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 478                    uLogger.debug("    - not oK, {}".format(errMsg))
 479
 480                    if "code" in response.text and "message" in response.text:
 481                        errMsgDict = self._ParseJSON(rawData=response.text)
 482                        uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 483
 484                    counter += 1
 485
 486                    if counter <= retry:
 487                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 488                        sleep(pause)
 489
 490            responseJSON = self._ParseJSON(rawData=response.text)
 491
 492            if errMsg:
 493                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 494                uLogger.error("    - not oK, {}".format(errMsg))
 495
 496        return responseJSON
 497
 498    def _IUpdater(self, iType: str) -> tuple:
 499        """
 500        Request instrument by type from server. See available API methods for instruments:
 501        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 502        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 503        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 504        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 505        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 506
 507        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 508        :return: tuple with iType name and list of available instruments of current type for defined user token.
 509        """
 510        result = []
 511
 512        if iType in TKS_INSTRUMENTS:
 513            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 514
 515            # all instruments have the same body in API v2 requests:
 516            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 517            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 518            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 519
 520        return iType, result
 521
 522    def _IWrapper(self, kwargs):
 523        """
 524        Wrapper runs instrument's update method `_IUpdater()`.
 525        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 526        """
 527        return self._IUpdater(**kwargs)
 528
 529    def Listing(self) -> dict:
 530        """
 531        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 532
 533        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 534        """
 535        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 536        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 537
 538        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 539        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 540        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 541
 542        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 543        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 544        poolUpdater.close()
 545
 546        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 547        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 548        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 549
 550        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 551        for iType in iList.keys():
 552            for ticker in iList[iType]:
 553                iList[iType][ticker]["type"] = iType
 554
 555                if "minPriceIncrement" in iList[iType][ticker].keys():
 556                    iList[iType][ticker]["step"] = NanoToFloat(
 557                        iList[iType][ticker]["minPriceIncrement"]["units"],
 558                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 559                    )
 560
 561                else:
 562                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 563
 564        return iList
 565
 566    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 567        """
 568        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 569
 570        See also: `DumpInstruments()`, `Listing()`.
 571
 572        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 573                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 574        """
 575        if self.iListDumpFile is None or not self.iListDumpFile:
 576            uLogger.error("Output name of dump file must be defined!")
 577            raise Exception("Filename required")
 578
 579        if not self.iList or forceUpdate:
 580            self.iList = self.Listing()
 581
 582        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 583
 584        # Save as XLSX with separated sheets for every type of instruments:
 585        with pd.ExcelWriter(
 586                path=xlsxDumpFile,
 587                date_format=TKS_DATE_FORMAT,
 588                datetime_format=TKS_DATE_TIME_FORMAT,
 589                mode="w",
 590        ) as writer:
 591            for iType in TKS_INSTRUMENTS:
 592                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 593                df = df[sorted(df)]  # sorted by column names
 594                df = df.applymap(
 595                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 596                    na_action="ignore",
 597                )  # converting numbers from nano-type to float in every cell
 598                df.to_excel(
 599                    writer,
 600                    sheet_name=iType,
 601                    encoding="UTF-8",
 602                    freeze_panes=(1, 1),
 603                )  # saving as XLSX-file with freeze first row and column as headers
 604
 605        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 606
 607    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 608        """
 609        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 610        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 611
 612        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 613
 614        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 615                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 616        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 617        """
 618        if self.iListDumpFile is None or not self.iListDumpFile:
 619            uLogger.error("Output name of dump file must be defined!")
 620            raise Exception("Filename required")
 621
 622        if not self.iList or forceUpdate:
 623            self.iList = self.Listing()
 624
 625        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 626        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 627            fH.write(jsonDump)
 628
 629        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 630
 631        return jsonDump
 632
 633    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 634        """
 635        Show information about one instrument defined by json data and prints it in Markdown format.
 636
 637        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 638
 639        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 640        :param show: if `True` then also printing information about instrument and its current price.
 641        :return: multilines text in Markdown format with information about one instrument.
 642        """
 643        splitLine = "|                                                             |                                                        |\n"
 644        infoText = ""
 645
 646        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 647            info = [
 648                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 649                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 650                "| Parameters                                                  | Values                                                 |\n",
 651                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 652                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 653                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 654            ]
 655
 656            if "sector" in iJSON.keys() and iJSON["sector"]:
 657                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 658
 659            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 660                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 661
 662            info.extend([
 663                splitLine,
 664                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 665                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 666            ])
 667
 668            if "isin" in iJSON.keys() and iJSON["isin"]:
 669                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 670
 671            if "classCode" in iJSON.keys():
 672                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 673
 674            info.extend([
 675                splitLine,
 676                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 677                splitLine,
 678                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 679                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 680                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 681            ])
 682
 683            if iJSON["figi"]:
 684                self._figi = iJSON["figi"]
 685                iJSON = iJSON | self.RequestTradingStatus()
 686
 687                info.extend([
 688                    splitLine,
 689                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 690                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 691                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 692                ])
 693
 694            info.append(splitLine)
 695
 696            if "type" in iJSON.keys() and iJSON["type"]:
 697                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 698
 699                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 700                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 701
 702            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 703                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 704
 705            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 706                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 707
 708            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 709                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 710
 711            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 712                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 713
 714            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 715                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 716
 717            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 718                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 719
 720            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 721                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 722
 723            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 724                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 725
 726            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 727                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 728
 729            if "currency" in iJSON.keys():
 730                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 731
 732            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 733                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 734
 735            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 736                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 737
 738            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 739                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 740
 741            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 742                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 743
 744            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 745                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 746
 747            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 748                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 749
 750            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 751                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 752
 753            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 754                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 755
 756            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 757                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 758
 759            iExt = None
 760            if iJSON["type"] == "Bonds":
 761                info.extend([
 762                    splitLine,
 763                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 764                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 765                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 766                        iJSON["nominal"]["currency"],
 767                    )),
 768                ])
 769
 770                if "floatingCouponFlag" in iJSON.keys():
 771                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 772
 773                if "amortizationFlag" in iJSON.keys():
 774                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 775
 776                info.append(splitLine)
 777
 778                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 779                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 780
 781                if iJSON["figi"]:
 782                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 783
 784                    info.extend([
 785                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 786                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 787                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 788                    ])
 789
 790                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 791                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 792                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 793                        iJSON["aciValue"]["currency"]
 794                    )))
 795
 796            if "currentPrice" in iJSON.keys():
 797                info.append(splitLine)
 798
 799                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 800                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 801
 802                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 803                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 804                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 805                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 806                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 807
 808                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 809                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 810
 811                info.extend([
 812                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 813                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 814                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 815                    )),
 816                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 817                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 818                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 819                    )),
 820                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 821                        "{:.2f}%{}".format(
 822                            iJSON["currentPrice"]["changes"],
 823                            " ({}{:.2f} {})".format(
 824                                "+" if bondChangesDelta > 0 else "",
 825                                bondChangesDelta,
 826                                aciCurrency
 827                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 828                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 829                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 830                                currency
 831                            ),
 832                        )
 833                    ),
 834                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 835                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 836                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 837                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 838                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 839                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 840                    )),
 841                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 842                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 843                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 844                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 845                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 846                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 847                    )),
 848                ])
 849
 850            if "lot" in iJSON.keys():
 851                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 852
 853            if "step" in iJSON.keys() and iJSON["step"] != 0:
 854                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 855
 856            # Add bond payment calendar:
 857            if iJSON["type"] == "Bonds":
 858                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 859                info.extend(["\n", strCalendar])
 860
 861            infoText += "".join(info)
 862
 863            if show:
 864                uLogger.info("{}".format(infoText))
 865
 866            else:
 867                uLogger.debug("{}".format(infoText))
 868
 869            if self.infoFile is not None:
 870                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 871                    fH.write(infoText)
 872
 873                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 874
 875        return infoText
 876
 877    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 878        """
 879        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 880
 881        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 882        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 883        :return: JSON formatted data with information about instrument.
 884        """
 885        tickerJSON = {}
 886        if self.moreDebug:
 887            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 888
 889        if not self._ticker:
 890            uLogger.warning("self._ticker variable is not be empty!")
 891
 892        else:
 893            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 894                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 895                raise Exception("Instrument not allowed")
 896
 897            if not self.iList:
 898                self.iList = self.Listing()
 899
 900            if self._ticker in self.iList["Shares"].keys():
 901                tickerJSON = self.iList["Shares"][self._ticker]
 902                if self.moreDebug:
 903                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 904
 905            elif self._ticker in self.iList["Currencies"].keys():
 906                tickerJSON = self.iList["Currencies"][self._ticker]
 907                if self.moreDebug:
 908                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 909
 910            elif self._ticker in self.iList["Bonds"].keys():
 911                tickerJSON = self.iList["Bonds"][self._ticker]
 912                if self.moreDebug:
 913                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 914
 915            elif self._ticker in self.iList["Etfs"].keys():
 916                tickerJSON = self.iList["Etfs"][self._ticker]
 917                if self.moreDebug:
 918                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 919
 920            elif self._ticker in self.iList["Futures"].keys():
 921                tickerJSON = self.iList["Futures"][self._ticker]
 922                if self.moreDebug:
 923                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 924
 925        if tickerJSON:
 926            self._figi = tickerJSON["figi"]
 927
 928            if requestPrice:
 929                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 930
 931                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 932                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 933
 934                else:
 935                    tickerJSON["currentPrice"]["changes"] = 0
 936
 937            if show:
 938                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 939
 940        else:
 941            if show:
 942                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 943
 944        return tickerJSON
 945
 946    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 947        """
 948        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 949
 950        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 951        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 952        :return: JSON formatted data with information about instrument.
 953        """
 954        figiJSON = {}
 955        if self.moreDebug:
 956            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 957
 958        if not self._figi:
 959            uLogger.warning("self._figi variable is not be empty!")
 960
 961        else:
 962            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 963                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 964                raise Exception("Instrument not allowed")
 965
 966            if not self.iList:
 967                self.iList = self.Listing()
 968
 969            for item in self.iList["Shares"].keys():
 970                if self._figi == self.iList["Shares"][item]["figi"]:
 971                    figiJSON = self.iList["Shares"][item]
 972
 973                    if self.moreDebug:
 974                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
 975
 976                    break
 977
 978            if not figiJSON:
 979                for item in self.iList["Currencies"].keys():
 980                    if self._figi == self.iList["Currencies"][item]["figi"]:
 981                        figiJSON = self.iList["Currencies"][item]
 982
 983                        if self.moreDebug:
 984                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
 985
 986                        break
 987
 988            if not figiJSON:
 989                for item in self.iList["Bonds"].keys():
 990                    if self._figi == self.iList["Bonds"][item]["figi"]:
 991                        figiJSON = self.iList["Bonds"][item]
 992
 993                        if self.moreDebug:
 994                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
 995
 996                        break
 997
 998            if not figiJSON:
 999                for item in self.iList["Etfs"].keys():
1000                    if self._figi == self.iList["Etfs"][item]["figi"]:
1001                        figiJSON = self.iList["Etfs"][item]
1002
1003                        if self.moreDebug:
1004                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1005
1006                        break
1007
1008            if not figiJSON:
1009                for item in self.iList["Futures"].keys():
1010                    if self._figi == self.iList["Futures"][item]["figi"]:
1011                        figiJSON = self.iList["Futures"][item]
1012
1013                        if self.moreDebug:
1014                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1015
1016                        break
1017
1018        if figiJSON:
1019            self._figi = figiJSON["figi"]
1020            self._ticker = figiJSON["ticker"]
1021
1022            if requestPrice:
1023                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1024
1025                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1026                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1027
1028                else:
1029                    figiJSON["currentPrice"]["changes"] = 0
1030
1031            if show:
1032                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1033
1034        else:
1035            if show:
1036                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1037
1038        return figiJSON
1039
1040    def GetCurrentPrices(self, show: bool = True) -> dict:
1041        """
1042        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1043        `{"buy": [{"price": 1243.8, "quantity": 193},
1044                  {"price": 1244.0, "quantity": 168},
1045                  {"price": 1244.8, "quantity": 5},
1046                  {"price": 1245.0, "quantity": 61},
1047                  {"price": 1245.4, "quantity": 60}],
1048          "sell": [{"price": 1243.6, "quantity": 8},
1049                   {"price": 1242.6, "quantity": 10},
1050                   {"price": 1242.4, "quantity": 18},
1051                   {"price": 1242.2, "quantity": 50},
1052                   {"price": 1242.0, "quantity": 113}],
1053          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1054        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1055        - sell: list of dicts with Buyers prices,
1056            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1057            - quantity: volume value by current price in lots,
1058        - limitUp: current trade session limit price, maximum,
1059        - limitDown: current trade session limit price, minimum,
1060        - lastPrice: last deal price of the instrument,
1061        - closePrice: previous trade session close price of the instrument.
1062
1063        See also: `SearchByTicker()` and `SearchByFIGI()`.
1064        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1065        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1066
1067        :param show: if `True` then print DOM to log and console.
1068        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1069                 If an error occurred then returns an empty record:
1070                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1071        """
1072        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1073
1074        if self.depth < 1:
1075            uLogger.error("Depth of Market (DOM) must be >=1!")
1076            raise Exception("Incorrect value")
1077
1078        if not (self._ticker or self._figi):
1079            uLogger.error("self._ticker or self._figi variables must be defined!")
1080            raise Exception("Ticker or FIGI required")
1081
1082        if self._ticker and not self._figi:
1083            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1084            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1085
1086        if not self._ticker and self._figi:
1087            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1088            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1089
1090        if not self._figi:
1091            uLogger.error("FIGI is not defined!")
1092            raise Exception("Ticker or FIGI required")
1093
1094        else:
1095            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1096
1097            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1098            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1099            self.body = str({"figi": self._figi, "depth": self.depth})
1100            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1101
1102            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1103                # list of dicts with sellers orders:
1104                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1105
1106                # list of dicts with buyers orders:
1107                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1108
1109                # max price of instrument at this time:
1110                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1111
1112                # min price of instrument at this time:
1113                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1114
1115                # last price of deal with instrument:
1116                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1117
1118                # last close price of instrument:
1119                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1120
1121            else:
1122                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1123                uLogger.debug("Server response: {}".format(pricesResponse))
1124
1125            if show:
1126                if prices["buy"] or prices["sell"]:
1127                    info = [
1128                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1129                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1130                            self._ticker,
1131                            self._figi,
1132                            self.depth,
1133                        ),
1134                        "-" * 60, "\n",
1135                        "             Orders of Buyers | Orders of Sellers\n",
1136                        "-" * 60, "\n",
1137                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1138                        "-" * 60, "\n",
1139                    ]
1140
1141                    if not prices["buy"]:
1142                        info.append("                              | No orders!\n")
1143                        sumBuy = 0
1144
1145                    else:
1146                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1147                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1148                        for item in maxMinSorted:
1149                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1150
1151                    if not prices["sell"]:
1152                        info.append("No orders!                    |\n")
1153                        sumSell = 0
1154
1155                    else:
1156                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1157                        for item in prices["sell"]:
1158                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1159
1160                    info.extend([
1161                        "-" * 60, "\n",
1162                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1163                        "-" * 60, "\n",
1164                    ])
1165
1166                    infoText = "".join(info)
1167
1168                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1169
1170                else:
1171                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1172
1173        return prices
1174
1175    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1176        """
1177        This method get and show information about all available broker instruments for current user account.
1178        If `instrumentsFile` string is not empty then also save information to this file.
1179
1180        :param show: if `True` then print results to console, if `False` — print only to file.
1181        :return: multi-lines string with all available broker instruments
1182        """
1183        if not self.iList:
1184            self.iList = self.Listing()
1185
1186        info = [
1187            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1188            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1189        ]
1190
1191        # add instruments count by type:
1192        for iType in self.iList.keys():
1193            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1194
1195        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1196        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1197
1198        # generating info tables with all instruments by type:
1199        for iType in self.iList.keys():
1200            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1201
1202            for instrument in self.iList[iType].keys():
1203                iName = self.iList[iType][instrument]["name"]  # instrument's name
1204                if len(iName) > 57:
1205                    iName = "{}...".format(iName[:54])  # right trim for a long string
1206
1207                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1208                    self.iList[iType][instrument]["ticker"],
1209                    iName,
1210                    self.iList[iType][instrument]["figi"],
1211                    self.iList[iType][instrument]["currency"],
1212                    self.iList[iType][instrument]["lot"],
1213                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1214                ))
1215
1216        infoText = "".join(info)
1217
1218        if show:
1219            uLogger.info(infoText)
1220
1221        if self.instrumentsFile:
1222            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1223                fH.write(infoText)
1224
1225            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1226
1227        return infoText
1228
1229    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1230        """
1231        This method search and show information about instruments by part of its ticker, FIGI or name.
1232        If `searchResultsFile` string is not empty then also save information to this file.
1233
1234        :param pattern: string with part of ticker, FIGI or instrument's name.
1235        :param show: if `True` then print results to console, if `False` — return list of result only.
1236        :return: list of dictionaries with all found instruments.
1237        """
1238        if not self.iList:
1239            self.iList = self.Listing()
1240
1241        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1242        compiledPattern = re.compile(pattern, re.IGNORECASE)
1243
1244        for iType in self.iList:
1245            for instrument in self.iList[iType].values():
1246                searchResult = compiledPattern.search(" ".join(
1247                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1248                ))
1249
1250                if searchResult:
1251                    searchResults[iType][instrument["ticker"]] = instrument
1252
1253        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1254        info = [
1255            "# Search results\n\n",
1256            "* **Search pattern:** [{}]\n".format(pattern),
1257            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1258            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1259        ]
1260        infoShort = info[:]
1261
1262        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1263        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1264        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1265
1266        if resultsLen == 0:
1267            info.append("\nNo results\n")
1268            infoShort.append("\nNo results\n")
1269            uLogger.warning("No results. Try changing your search pattern.")
1270
1271        else:
1272            for iType in searchResults:
1273                iTypeValuesCount = len(searchResults[iType].values())
1274                if iTypeValuesCount > 0:
1275                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1276                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1277
1278                    for instrument in searchResults[iType].values():
1279                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1280                            instrument["type"],
1281                            instrument["ticker"],
1282                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1283                            instrument["figi"],
1284                        ))
1285
1286                    if iTypeValuesCount <= 5:
1287                        infoShort.extend(info[-iTypeValuesCount:])
1288
1289                    else:
1290                        infoShort.extend(info[-5:])
1291                        infoShort.append(skippedLine)
1292
1293        infoText = "".join(info)
1294        infoTextShort = "".join(infoShort)
1295
1296        if show:
1297            uLogger.info(infoTextShort)
1298            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1299
1300        if self.searchResultsFile:
1301            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1302                fH.write(infoText)
1303
1304            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1305
1306        return searchResults
1307
1308    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1309        """
1310        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1311
1312        :param instruments: list of strings with tickers or FIGIs.
1313        :return: list with unique instrument FIGIs only.
1314        """
1315        requestedInstruments = []
1316        for iName in instruments:
1317            if iName not in self.aliases.keys():
1318                if iName not in requestedInstruments:
1319                    requestedInstruments.append(iName)
1320
1321            else:
1322                if iName not in requestedInstruments:
1323                    if self.aliases[iName] not in requestedInstruments:
1324                        requestedInstruments.append(self.aliases[iName])
1325
1326        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1327
1328        onlyUniqueFIGIs = []
1329        for iName in requestedInstruments:
1330            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1331                continue
1332
1333            self._ticker = iName
1334            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1335
1336            if not iData:
1337                self._ticker = ""
1338                self._figi = iName
1339
1340                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1341
1342                if not iData:
1343                    self._figi = ""
1344                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1345
1346            if iData and iData["figi"] not in onlyUniqueFIGIs:
1347                onlyUniqueFIGIs.append(iData["figi"])
1348
1349        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1350
1351        return onlyUniqueFIGIs
1352
1353    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1354        """
1355        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1356
1357        See limits: https://tinkoff.github.io/investAPI/limits/
1358
1359        If `pricesFile` string is not empty then also save information to this file.
1360
1361        :param instruments: list of strings with tickers or FIGIs.
1362        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1363        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1364                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1365        """
1366        if instruments is None or not instruments:
1367            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1368            raise Exception("Ticker or FIGI required")
1369
1370        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1371
1372        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1373
1374        iList = []  # trying to get info and current prices about all unique instruments:
1375        for self._figi in onlyUniqueFIGIs:
1376            iData = self.SearchByFIGI(requestPrice=True)
1377            iList.append(iData)
1378
1379        self.ShowListOfPrices(iList, show)
1380
1381        return iList
1382
1383    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1384        """
1385        Show table contains current prices of given instruments.
1386
1387        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1388                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1389        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1390        :return: multilines text in Markdown format as a table contains current prices.
1391        """
1392        infoText = ""
1393
1394        if show or self.pricesFile:
1395            info = [
1396                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1397                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1398                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1399            ]
1400
1401            for item in iList:
1402                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1403                    item["ticker"],
1404                    item["figi"],
1405                    item["type"],
1406                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1407                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1408                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1409                    "{} / {}".format(
1410                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1411                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1412                    ),
1413                    "{} / {}".format(
1414                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1415                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1416                    ),
1417                    item["currency"],
1418                ))
1419
1420            infoText = "".join(info)
1421
1422            if show:
1423                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1424
1425            if self.pricesFile:
1426                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1427                    fH.write(infoText)
1428
1429                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1430
1431        return infoText
1432
1433    def RequestTradingStatus(self) -> dict:
1434        """
1435        Requesting trading status for the instrument defined by `figi` variable.
1436
1437        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1438
1439        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1440
1441        :return: dictionary with trading status attributes. Response example:
1442                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1443                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1444        """
1445        if self._figi is None or not self._figi:
1446            uLogger.error("Variable `figi` must be defined for using this method!")
1447            raise Exception("FIGI required")
1448
1449        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1450
1451        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1452        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1453        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1454
1455        if self.moreDebug:
1456            uLogger.debug("Records about current trading status successfully received")
1457
1458        return tradingStatus
1459
1460    def RequestPortfolio(self) -> dict:
1461        """
1462        Requesting actual user's portfolio for current `accountId`.
1463
1464        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1465
1466        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1467
1468        :return: dictionary with user's portfolio.
1469        """
1470        if self.accountId is None or not self.accountId:
1471            uLogger.error("Variable `accountId` must be defined for using this method!")
1472            raise Exception("Account ID required")
1473
1474        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1475
1476        self.body = str({"accountId": self.accountId})
1477        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1478        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1479
1480        if self.moreDebug:
1481            uLogger.debug("Records about user's portfolio successfully received")
1482
1483        return rawPortfolio
1484
1485    def RequestPositions(self) -> dict:
1486        """
1487        Requesting open positions by currencies and instruments for current `accountId`.
1488
1489        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1490
1491        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1492
1493        :return: dictionary with open positions by instruments.
1494        """
1495        if self.accountId is None or not self.accountId:
1496            uLogger.error("Variable `accountId` must be defined for using this method!")
1497            raise Exception("Account ID required")
1498
1499        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1500
1501        self.body = str({"accountId": self.accountId})
1502        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1503        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1504
1505        if self.moreDebug:
1506            uLogger.debug("Records about current open positions successfully received")
1507
1508        return rawPositions
1509
1510    def RequestPendingOrders(self) -> list:
1511        """
1512        Requesting current actual pending limit orders for current `accountId`.
1513
1514        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1515
1516        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1517
1518        :return: list of dictionaries with pending limit orders.
1519        """
1520        if self.accountId is None or not self.accountId:
1521            uLogger.error("Variable `accountId` must be defined for using this method!")
1522            raise Exception("Account ID required")
1523
1524        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1525
1526        self.body = str({"accountId": self.accountId})
1527        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1528        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1529
1530        uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1531
1532        return rawOrders
1533
1534    def RequestStopOrders(self) -> list:
1535        """
1536        Requesting current actual stop orders for current `accountId`.
1537
1538        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1539
1540        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1541
1542        :return: list of dictionaries with stop orders.
1543        """
1544        if self.accountId is None or not self.accountId:
1545            uLogger.error("Variable `accountId` must be defined for using this method!")
1546            raise Exception("Account ID required")
1547
1548        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1549
1550        self.body = str({"accountId": self.accountId})
1551        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1552        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1553
1554        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1555
1556        return rawStopOrders
1557
1558    def Overview(self, show: bool = False, details: str = "full") -> dict:
1559        """
1560        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1561        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1562        and `overviewBondsCalendarFile` are defined then also save information to file.
1563
1564        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1565        many requests about the state of the portfolio, and then, based on the received data, a large number
1566        of calculation and statistics are collected.
1567
1568        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1569        :param details: how detailed should the information be?
1570        - `full` — shows full available information about portfolio status (by default),
1571        - `positions` — shows only open positions,
1572        - `orders` — shows only sections of open limits and stop orders.
1573        - `digest` — show a short digest of the portfolio status,
1574        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1575        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1576        :return: dictionary with client's raw portfolio and some statistics.
1577        """
1578        if self.accountId is None or not self.accountId:
1579            uLogger.error("Variable `accountId` must be defined for using this method!")
1580            raise Exception("Account ID required")
1581
1582        view = {
1583            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1584                "headers": {},  # list of dictionaries, response headers without "positions" section
1585                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1586                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1587                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1588                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1589                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1590                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1591                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1592                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1593                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1594            },
1595            "stat": {  # --- some statistics calculated using "raw" sections:
1596                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1597                "availableRUB": 0.,  # available rubles (without other currencies)
1598                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1599                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1600                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1601                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1602                "sharesCostRUB": 0.,  # costs of all shares in RUB
1603                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1604                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1605                "futuresCostRUB": 0.,  # costs of all futures in RUB
1606                "Currencies": [],  # list of dictionaries of all currencies statistics
1607                "Shares": [],  # list of dictionaries of all shares statistics
1608                "Bonds": [],  # list of dictionaries of all bonds statistics
1609                "Etfs": [],  # list of dictionaries of all etfs statistics
1610                "Futures": [],  # list of dictionaries of all futures statistics
1611                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1612                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1613                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1614                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1615                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1616            },
1617            "analytics": {  # --- some analytics of portfolio:
1618                "distrByAssets": {},  # portfolio distribution by assets
1619                "distrByCompanies": {},  # portfolio distribution by companies
1620                "distrBySectors": {},  # portfolio distribution by sectors
1621                "distrByCurrencies": {},  # portfolio distribution by currencies
1622                "distrByCountries": {},  # portfolio distribution by countries
1623                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1624            }
1625        }
1626
1627        details = details.lower()
1628        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1629        if details not in availableDetails:
1630            details = "full"
1631            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1632
1633        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1634
1635        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1636        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1637        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1638        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1639
1640        # save response headers without "positions" section:
1641        for key in portfolioResponse.keys():
1642            if key != "positions":
1643                view["raw"]["headers"][key] = portfolioResponse[key]
1644
1645            else:
1646                continue
1647
1648        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1649        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1650        for item in portfolioResponse["positions"]:
1651            if item["instrumentType"] == "currency":
1652                self._figi = item["figi"]
1653                curr = self.SearchByFIGI(requestPrice=False)
1654
1655                # current price of currency in RUB:
1656                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1657                    "name": curr["name"],
1658                    "currentPrice": NanoToFloat(
1659                        item["currentPrice"]["units"],
1660                        item["currentPrice"]["nano"]
1661                    ),
1662                }
1663
1664                view["raw"]["Currencies"].append(item)
1665
1666            elif item["instrumentType"] == "share":
1667                view["raw"]["Shares"].append(item)
1668
1669            elif item["instrumentType"] == "bond":
1670                view["raw"]["Bonds"].append(item)
1671
1672            elif item["instrumentType"] == "etf":
1673                view["raw"]["Etfs"].append(item)
1674
1675            elif item["instrumentType"] == "futures":
1676                view["raw"]["Futures"].append(item)
1677
1678            else:
1679                continue
1680
1681        # how many volume of currencies (by ISO currency name) are blocked:
1682        for item in view["raw"]["positions"]["blocked"]:
1683            blocked = NanoToFloat(item["units"], item["nano"])
1684            if blocked > 0:
1685                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1686
1687        # how many volume of instruments (by FIGI) are blocked:
1688        for item in view["raw"]["positions"]["securities"]:
1689            blocked = int(item["blocked"])
1690            if blocked > 0:
1691                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1692
1693        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1694
1695        if "rub" in allBlocked.keys():
1696            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1697
1698        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1699        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1700        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1701        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1702        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1703        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1704        view["stat"]["portfolioCostRUB"] = sum([
1705            view["stat"]["allCurrenciesCostRUB"],
1706            view["stat"]["sharesCostRUB"],
1707            view["stat"]["bondsCostRUB"],
1708            view["stat"]["etfsCostRUB"],
1709            view["stat"]["futuresCostRUB"],
1710        ])
1711
1712        # --- calculating some portfolio statistics:
1713        byComp = {}  # distribution by companies
1714        bySect = {}  # distribution by sectors
1715        byCurr = {}  # distribution by currencies (include RUB)
1716        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1717        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1718
1719        for item in portfolioResponse["positions"]:
1720            self._figi = item["figi"]
1721            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1722
1723            if instrument:
1724                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1725                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1726
1727                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1728                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1729
1730                else:
1731                    blocked = 0
1732
1733                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1734                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1735                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1736                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1737                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1738                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1739                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1740                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1741                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1742                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1743                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1744                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1745
1746                statData = {
1747                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1748                    "ticker": instrument["ticker"],  # ticker by FIGI
1749                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1750                    "volume": volume,  # available volume of instrument
1751                    "lots": lots,  # volume in lots of instrument
1752                    "direction": direction,  # direction of an instrument's position: short or long
1753                    "blocked": blocked,  # blocked volume of currency or instrument
1754                    "currentPrice": curPrice,  # current instrument's price in basic asset
1755                    "average": average,  # current average position price
1756                    "cost": cost,  # current cost of all volume of instrument in basic asset
1757                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1758                    "costRUB": costRUB,  # cost of instrument in ruble
1759                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1760                    "profit": profit,  # expected profit at current moment
1761                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1762                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1763                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1764                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1765                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1766                    "step": instrument["step"],  # minimum price increment
1767                }
1768
1769                # adding distribution by unique countries:
1770                if statData["country"] not in byCountry.keys():
1771                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1772
1773                else:
1774                    byCountry[statData["country"]]["cost"] += costRUB
1775                    byCountry[statData["country"]]["percent"] += percentCostRUB
1776
1777                if item["instrumentType"] != "currency":
1778                    # adding distribution by unique companies:
1779                    if statData["name"]:
1780                        if statData["name"] not in byComp.keys():
1781                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1782
1783                        else:
1784                            byComp[statData["name"]]["cost"] += costRUB
1785                            byComp[statData["name"]]["percent"] += percentCostRUB
1786
1787                    # adding distribution by unique sectors:
1788                    if statData["sector"] not in bySect.keys():
1789                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1790
1791                    else:
1792                        bySect[statData["sector"]]["cost"] += costRUB
1793                        bySect[statData["sector"]]["percent"] += percentCostRUB
1794
1795                # adding distribution by unique currencies:
1796                if currency not in byCurr.keys():
1797                    byCurr[currency] = {
1798                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1799                        "cost": costRUB,
1800                        "percent": percentCostRUB
1801                    }
1802
1803                else:
1804                    byCurr[currency]["cost"] += costRUB
1805                    byCurr[currency]["percent"] += percentCostRUB
1806
1807                # saving statistics for every instrument:
1808                if item["instrumentType"] == "currency":
1809                    view["stat"]["Currencies"].append(statData)
1810
1811                    # update dict with free funds for trading (total - blocked) by currencies
1812                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1813                    view["stat"]["funds"][currency] = {
1814                        "total": volume,
1815                        "totalCostRUB": costRUB,  # total volume cost in rubles
1816                        "free": volume - blocked,
1817                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1818                    }
1819
1820                elif item["instrumentType"] == "share":
1821                    view["stat"]["Shares"].append(statData)
1822
1823                elif item["instrumentType"] == "bond":
1824                    view["stat"]["Bonds"].append(statData)
1825
1826                elif item["instrumentType"] == "etf":
1827                    view["stat"]["Etfs"].append(statData)
1828
1829                elif item["instrumentType"] == "Futures":
1830                    view["stat"]["Futures"].append(statData)
1831
1832                else:
1833                    continue
1834
1835        # total changes in Russian Ruble:
1836        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1837        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1838        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1839        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1840        view["stat"]["funds"]["rub"] = {
1841            "total": view["stat"]["availableRUB"],
1842            "totalCostRUB": view["stat"]["availableRUB"],
1843            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1844            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1845        }
1846
1847        # --- pending limit orders sector data:
1848        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1849        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1850
1851        for item in view["raw"]["orders"]:
1852            self._figi = item["figi"]
1853
1854            if item["figi"] not in uniquePendingOrdersFIGIs:
1855                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1856
1857                uniquePendingOrdersFIGIs.append(item["figi"])
1858                uniquePendingOrders[item["figi"]] = instrument
1859
1860            else:
1861                instrument = uniquePendingOrders[item["figi"]]
1862
1863            if instrument:
1864                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1865                orderType = TKS_ORDER_TYPES[item["orderType"]]
1866                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1867                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1868
1869                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1870                if item["direction"] == "ORDER_DIRECTION_BUY":
1871                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1872
1873                else:
1874                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1875
1876                # requested price for order execution:
1877                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1878
1879                # necessary changes in percent to reach target from current price:
1880                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1881
1882                view["stat"]["orders"].append({
1883                    "orderID": item["orderId"],  # orderId number parameter of current order
1884                    "figi": item["figi"],  # FIGI identification
1885                    "ticker": instrument["ticker"],  # ticker name by FIGI
1886                    "lotsRequested": item["lotsRequested"],  # requested lots value
1887                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1888                    "currentPrice": lastPrice,  # current instrument's price for defined action
1889                    "targetPrice": target,  # requested price for order execution in base currency
1890                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1891                    "percentChanges": changes,  # changes in percent to target from current price
1892                    "currency": item["currency"],  # instrument's currency name
1893                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1894                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1895                    "status": orderState,  # order status from TKS_ORDER_STATES
1896                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1897                })
1898
1899        # --- stop orders sector data:
1900        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1901        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1902
1903        for item in view["raw"]["stopOrders"]:
1904            self._figi = item["figi"]
1905
1906            if item["figi"] not in uniqueStopOrdersFIGIs:
1907                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1908
1909                uniqueStopOrdersFIGIs.append(item["figi"])
1910                uniqueStopOrders[item["figi"]] = instrument
1911
1912            else:
1913                instrument = uniqueStopOrders[item["figi"]]
1914
1915            if instrument:
1916                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1917                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1918                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1919
1920                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1921                if "expirationTime" in item.keys():
1922                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1923                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1924
1925                else:
1926                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1927                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1928
1929                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1930                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1931                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1932
1933                else:
1934                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1935
1936                # requested price when stop-order executed:
1937                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1938
1939                # price for limit-order, set up when stop-order executed:
1940                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1941
1942                # necessary changes in percent to reach target from current price:
1943                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1944
1945                view["stat"]["stopOrders"].append({
1946                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1947                    "figi": item["figi"],  # FIGI identification
1948                    "ticker": instrument["ticker"],  # ticker name by FIGI
1949                    "lotsRequested": item["lotsRequested"],  # requested lots value
1950                    "currentPrice": lastPrice,  # current instrument's price for defined action
1951                    "targetPrice": target,  # requested price for stop-order execution in base currency
1952                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1953                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1954                    "percentChanges": changes,  # changes in percent to target from current price
1955                    "currency": item["currency"],  # instrument's currency name
1956                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1957                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1958                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1959                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1960                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1961                })
1962
1963        # --- calculating data for analytics section:
1964        # portfolio distribution by assets:
1965        view["analytics"]["distrByAssets"] = {
1966            "Ruble": {
1967                "uniques": 1,
1968                "cost": view["stat"]["availableRUB"],
1969                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1970            },
1971            "Currencies": {
1972                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1973                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1974                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1975            },
1976            "Shares": {
1977                "uniques": len(view["stat"]["Shares"]),
1978                "cost": view["stat"]["sharesCostRUB"],
1979                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1980            },
1981            "Bonds": {
1982                "uniques": len(view["stat"]["Bonds"]),
1983                "cost": view["stat"]["bondsCostRUB"],
1984                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1985            },
1986            "Etfs": {
1987                "uniques": len(view["stat"]["Etfs"]),
1988                "cost": view["stat"]["etfsCostRUB"],
1989                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1990            },
1991            "Futures": {
1992                "uniques": len(view["stat"]["Futures"]),
1993                "cost": view["stat"]["futuresCostRUB"],
1994                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1995            },
1996        }
1997
1998        # portfolio distribution by companies:
1999        view["analytics"]["distrByCompanies"]["All money cash"] = {
2000            "ticker": "",
2001            "cost": view["stat"]["allCurrenciesCostRUB"],
2002            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2003        }
2004        view["analytics"]["distrByCompanies"].update(byComp)
2005
2006        # portfolio distribution by sectors:
2007        view["analytics"]["distrBySectors"]["All money cash"] = {
2008            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2009            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2010        }
2011        view["analytics"]["distrBySectors"].update(bySect)
2012
2013        # portfolio distribution by currencies:
2014        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2015            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2016
2017            if self.moreDebug:
2018                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2019
2020        view["analytics"]["distrByCurrencies"].update(byCurr)
2021        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2022        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2023
2024        # portfolio distribution by countries:
2025        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2026            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2027
2028            if self.moreDebug:
2029                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2030
2031        view["analytics"]["distrByCountries"].update(byCountry)
2032        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2033        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2034
2035        # --- Prepare text statistics overview in human-readable:
2036        if show:
2037            # Whatever the value `details`, header not changes:
2038            info = [
2039                "# Client's portfolio\n\n",
2040                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2041                "* **Account ID:** [{}]\n".format(self.accountId),
2042            ]
2043
2044            if details in ["full", "positions", "digest"]:
2045                info.extend([
2046                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2047                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2048                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2049                        view["stat"]["totalChangesRUB"],
2050                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2051                        view["stat"]["totalChangesPercentRUB"],
2052                    ),
2053                ])
2054
2055            if details in ["full", "positions"]:
2056                info.extend([
2057                    "## Open positions\n\n",
2058                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2059                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2060                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2061                        "{:.2f} ({:.2f}) rub".format(
2062                            view["stat"]["availableRUB"],
2063                            view["stat"]["blockedRUB"],
2064                        )
2065                    )
2066                ])
2067
2068                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2069                    return [
2070                        "|                             |                                 |          |              |              |                     |                              |\n",
2071                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2072                            noTradeStr if noTradeStr else typeStr,
2073                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2074                        ),
2075                    ]
2076
2077                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2078                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2079                        "{} [{}]".format(data["ticker"], data["figi"]),
2080                        "{:.2f} ({:.2f}) {}".format(
2081                            data["volume"],
2082                            data["blocked"],
2083                            data["currency"],
2084                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2085                            data["volume"],
2086                            data["blocked"],
2087                        ),
2088                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2089                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2090                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2091                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2092                        "{}{:.2f} {} ({}{:.2f}%)".format(
2093                            "+" if data["profit"] > 0 else "",
2094                            data["profit"], data["baseCurrencyName"],
2095                            "+" if data["percentProfit"] > 0 else "",
2096                            data["percentProfit"],
2097                        ),
2098                    )
2099
2100                # --- Show currencies section:
2101                if view["stat"]["Currencies"]:
2102                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2103                    for item in view["stat"]["Currencies"]:
2104                        info.append(_InfoStr(item, showCurrencyName=True))
2105
2106                else:
2107                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2108
2109                # --- Show shares section:
2110                if view["stat"]["Shares"]:
2111                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2112
2113                    for item in view["stat"]["Shares"]:
2114                        info.append(_InfoStr(item))
2115
2116                else:
2117                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2118
2119                # --- Show bonds section:
2120                if view["stat"]["Bonds"]:
2121                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2122
2123                    for item in view["stat"]["Bonds"]:
2124                        info.append(_InfoStr(item))
2125
2126                else:
2127                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2128
2129                # --- Show etfs section:
2130                if view["stat"]["Etfs"]:
2131                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2132
2133                    for item in view["stat"]["Etfs"]:
2134                        info.append(_InfoStr(item))
2135
2136                else:
2137                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2138
2139                # --- Show futures section:
2140                if view["stat"]["Futures"]:
2141                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2142
2143                    for item in view["stat"]["Futures"]:
2144                        info.append(_InfoStr(item))
2145
2146                else:
2147                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2148
2149            if details in ["full", "orders"]:
2150                # --- Show pending limit orders section:
2151                if view["stat"]["orders"]:
2152                    info.extend([
2153                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2154                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2155                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2156                    ])
2157
2158                    for item in view["stat"]["orders"]:
2159                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2160                            "{} [{}]".format(item["ticker"], item["figi"]),
2161                            item["orderID"],
2162                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2163                            "{} {} ({}{:.2f}%)".format(
2164                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2165                                item["baseCurrencyName"],
2166                                "+" if item["percentChanges"] > 0 else "",
2167                                float(item["percentChanges"]),
2168                            ),
2169                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2170                            item["action"],
2171                            item["type"],
2172                            item["date"],
2173                        ))
2174
2175                else:
2176                    info.append("\n## Total pending limit-orders: 0\n")
2177
2178                # --- Show stop orders section:
2179                if view["stat"]["stopOrders"]:
2180                    info.extend([
2181                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2182                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2183                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2184                    ])
2185
2186                    for item in view["stat"]["stopOrders"]:
2187                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2188                            "{} [{}]".format(item["ticker"], item["figi"]),
2189                            item["orderID"],
2190                            item["lotsRequested"],
2191                            "{} {} ({}{:.2f}%)".format(
2192                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2193                                item["baseCurrencyName"],
2194                                "+" if item["percentChanges"] > 0 else "",
2195                                float(item["percentChanges"]),
2196                            ),
2197                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2198                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2199                            item["action"],
2200                            item["type"],
2201                            item["expType"],
2202                            item["createDate"],
2203                            item["expDate"],
2204                        ))
2205
2206                else:
2207                    info.append("\n## Total stop-orders: 0\n")
2208
2209            if details in ["full", "analytics"]:
2210                # -- Show analytics section:
2211                if view["stat"]["portfolioCostRUB"] > 0:
2212                    info.extend([
2213                        "\n# Analytics\n"
2214                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2215                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2216                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2217                            view["stat"]["totalChangesRUB"],
2218                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2219                            view["stat"]["totalChangesPercentRUB"],
2220                        ),
2221                        "\n## Portfolio distribution by assets\n"
2222                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2223                        "|------------------------------------|---------|---------|--------------------|\n",
2224                    ])
2225
2226                    for key in view["analytics"]["distrByAssets"].keys():
2227                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2228                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2229                                key,
2230                                view["analytics"]["distrByAssets"][key]["uniques"],
2231                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2232                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2233                            ))
2234
2235                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2236
2237                    info.extend([
2238                        "\n## Portfolio distribution by companies\n"
2239                        "\n| Company                                      | Percent | Current cost       |\n",
2240                        aSepLine,
2241                    ])
2242
2243                    for company in view["analytics"]["distrByCompanies"].keys():
2244                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2245                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2246                                "{}{}".format(
2247                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2248                                    company,
2249                                ),
2250                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2251                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2252                            ))
2253
2254                    info.extend([
2255                        "\n## Portfolio distribution by sectors\n"
2256                        "\n| Sector                                       | Percent | Current cost       |\n",
2257                        aSepLine,
2258                    ])
2259
2260                    for sector in view["analytics"]["distrBySectors"].keys():
2261                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2262                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2263                                sector,
2264                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2265                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2266                            ))
2267
2268                    info.extend([
2269                        "\n## Portfolio distribution by currencies\n"
2270                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2271                        aSepLine,
2272                    ])
2273
2274                    for curr in view["analytics"]["distrByCurrencies"].keys():
2275                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2276                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2277                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2278                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2279                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2280                            ))
2281
2282                    info.extend([
2283                        "\n## Portfolio distribution by countries\n"
2284                        "\n| Assets by country                            | Percent | Current cost       |\n",
2285                        aSepLine,
2286                    ])
2287
2288                    for country in view["analytics"]["distrByCountries"].keys():
2289                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2290                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2291                                country,
2292                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2293                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2294                            ))
2295
2296            if details in ["full", "calendar"]:
2297                # -- Show bonds payment calendar section:
2298                if view["stat"]["Bonds"]:
2299                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2300                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2301                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2302
2303                else:
2304                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2305
2306            infoText = "".join(info)
2307
2308            uLogger.info(infoText)
2309
2310            if details == "full" and self.overviewFile:
2311                filename = self.overviewFile
2312
2313            elif details == "digest" and self.overviewDigestFile:
2314                filename = self.overviewDigestFile
2315
2316            elif details == "positions" and self.overviewPositionsFile:
2317                filename = self.overviewPositionsFile
2318
2319            elif details == "orders" and self.overviewOrdersFile:
2320                filename = self.overviewOrdersFile
2321
2322            elif details == "analytics" and self.overviewAnalyticsFile:
2323                filename = self.overviewAnalyticsFile
2324
2325            elif details == "calendar" and self.overviewBondsCalendarFile:
2326                filename = self.overviewBondsCalendarFile
2327
2328            else:
2329                filename = ""
2330
2331            if filename:
2332                with open(filename, "w", encoding="UTF-8") as fH:
2333                    fH.write(infoText)
2334
2335                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2336
2337        return view
2338
2339    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2340        """
2341        Returns history operations between two given dates for current `accountId`.
2342        If `reportFile` string is not empty then also save human-readable report.
2343        Shows some statistical data of closed positions.
2344
2345        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2346        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2347        :param show: if `True` then also prints all records to the console.
2348        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2349        :return: original list of dictionaries with history of deals records from API ("operations" key):
2350                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2351                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2352        """
2353        if self.accountId is None or not self.accountId:
2354            uLogger.error("Variable `accountId` must be defined for using this method!")
2355            raise Exception("Account ID required")
2356
2357        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2358
2359        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2360
2361        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2362        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2363        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2364        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2365        customStat = {}  # custom statistics in additional to responseJSON
2366
2367        # --- output report in human-readable format:
2368        if show or self.reportFile:
2369            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2370            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2371            nextDay = ""
2372
2373            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2374
2375            if len(ops) > 0:
2376                customStat = {
2377                    "opsCount": 0,  # total operations count
2378                    "buyCount": 0,  # buy operations
2379                    "sellCount": 0,  # sell operations
2380                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2381                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2382                    "payIn": {"rub": 0.},  # Deposit brokerage account
2383                    "payOut": {"rub": 0.},  # Withdrawals
2384                    "divs": {"rub": 0.},  # Dividends income
2385                    "coupons": {"rub": 0.},  # Coupon's income
2386                    "brokerCom": {"rub": 0.},  # Service commissions
2387                    "serviceCom": {"rub": 0.},  # Service commissions
2388                    "marginCom": {"rub": 0.},  # Margin commissions
2389                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2390                }
2391
2392                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2393                for item in ops:
2394                    if item["state"] == "OPERATION_STATE_EXECUTED":
2395                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2396
2397                        # count buy operations:
2398                        if "_BUY" in item["operationType"]:
2399                            customStat["buyCount"] += 1
2400
2401                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2402                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2403
2404                            else:
2405                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2406
2407                        # count sell operations:
2408                        elif "_SELL" in item["operationType"]:
2409                            customStat["sellCount"] += 1
2410
2411                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2412                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2413
2414                            else:
2415                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2416
2417                        # count incoming operations:
2418                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2419                            if item["payment"]["currency"] in customStat["payIn"].keys():
2420                                customStat["payIn"][item["payment"]["currency"]] += payment
2421
2422                            else:
2423                                customStat["payIn"][item["payment"]["currency"]] = payment
2424
2425                        # count withdrawals operations:
2426                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2427                            if item["payment"]["currency"] in customStat["payOut"].keys():
2428                                customStat["payOut"][item["payment"]["currency"]] += payment
2429
2430                            else:
2431                                customStat["payOut"][item["payment"]["currency"]] = payment
2432
2433                        # count dividends income:
2434                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2435                            if item["payment"]["currency"] in customStat["divs"].keys():
2436                                customStat["divs"][item["payment"]["currency"]] += payment
2437
2438                            else:
2439                                customStat["divs"][item["payment"]["currency"]] = payment
2440
2441                        # count coupon's income:
2442                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2443                            if item["payment"]["currency"] in customStat["coupons"].keys():
2444                                customStat["coupons"][item["payment"]["currency"]] += payment
2445
2446                            else:
2447                                customStat["coupons"][item["payment"]["currency"]] = payment
2448
2449                        # count broker commissions:
2450                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2451                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2452                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2453
2454                            else:
2455                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2456
2457                        # count service commissions:
2458                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2459                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2460                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2461
2462                            else:
2463                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2464
2465                        # count margin commissions:
2466                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2467                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2468                                customStat["marginCom"][item["payment"]["currency"]] += payment
2469
2470                            else:
2471                                customStat["marginCom"][item["payment"]["currency"]] = payment
2472
2473                        # count withholding taxes:
2474                        elif "_TAX" in item["operationType"]:
2475                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2476                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2477
2478                            else:
2479                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2480
2481                        else:
2482                            continue
2483
2484                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2485
2486                # --- view "Actions" lines:
2487                info.extend([
2488                    "| Report sections            |                               |                              |                      |                        |\n",
2489                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2490                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2491                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2492                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2493                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2494                    ),
2495                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2496                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2497                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2498                    ),
2499                ])
2500
2501                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2502                for key in opsKeys:
2503                    if key == "rub":
2504                        continue
2505
2506                    info.extend([
2507                        "|                            |                               | {:<28} |                      |                        |\n".format(
2508                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2509                        ),
2510                        "|                            |                               | {:<28} |                      |                        |\n".format(
2511                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2512                        ),
2513                    ])
2514
2515                info.append(splitLine1)
2516
2517                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2518                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2519                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2520                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2521                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2522                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2523                    )
2524
2525                # --- view "Payments" lines:
2526                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2527                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2528
2529                for key in paymentsKeys:
2530                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2531
2532                info.append(splitLine1)
2533
2534                # --- view "Commissions and taxes" lines:
2535                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2536                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2537
2538                for key in comKeys:
2539                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2540
2541                info.append(splitLine1)
2542
2543                info.extend([
2544                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2545                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2546                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2547                ])
2548
2549            else:
2550                info.append("Broker returned no operations during this period\n")
2551
2552            # --- view "Operations" section:
2553            for item in ops:
2554                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2555                    continue
2556
2557                else:
2558                    self._figi = item["figi"] if item["figi"] else ""
2559                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2560                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2561
2562                    # group of deals during one day:
2563                    if nextDay and item["date"].split("T")[0] != nextDay:
2564                        info.append(splitLine2)
2565                        nextDay = ""
2566
2567                    else:
2568                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2569
2570                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2571                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2572                        self._figi if self._figi else "—",
2573                        instrument["ticker"] if instrument else "—",
2574                        instrument["type"] if instrument else "—",
2575                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2576                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2577                        TKS_OPERATION_STATES[item["state"]],
2578                        TKS_OPERATION_TYPES[item["operationType"]],
2579                    ))
2580
2581            infoText = "".join(info)
2582
2583            if show:
2584                if self.moreDebug:
2585                    uLogger.debug("Records about history of a client's operations successfully received")
2586
2587                uLogger.info(infoText)
2588
2589            if self.reportFile:
2590                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2591                    fH.write(infoText)
2592
2593                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2594
2595        return ops, customStat
2596
2597    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2598        """
2599        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2600
2601        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2602        Warning! Broker server used ISO UTC time by default.
2603
2604        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2605        Also, `historyFile` used to update history with `onlyMissing` parameter.
2606
2607        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2608
2609        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2610        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2611        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2612                         `"hour"`, `"day"`. Default: `"hour"`.
2613        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2614                            False by default. Warning! History appends only from last candle to current time
2615                            with always update last candle!
2616        :param csvSep: separator if csv-file is used, `,` by default.
2617        :param show: if `True` then also prints Pandas DataFrame to the console.
2618        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2619                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2620        """
2621        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2622        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2623        history = None  # empty pandas object for history
2624
2625        if interval not in TKS_CANDLE_INTERVALS.keys():
2626            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2627            raise Exception("Incorrect value")
2628
2629        if not (self._ticker or self._figi):
2630            uLogger.error("Ticker or FIGI must be defined!")
2631            raise Exception("Ticker or FIGI required")
2632
2633        if self._ticker and not self._figi:
2634            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2635            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2636
2637        if self._figi and not self._ticker:
2638            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2639            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2640
2641        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2642        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2643        if interval.lower() != "day":
2644            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2645
2646        delta = dtEnd - dtStart  # current UTC time minus last time in file
2647        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2648
2649        # calculate history length in candles:
2650        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2651        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2652            length += 1  # to avoid fraction time
2653
2654        # calculate data blocks count:
2655        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2656
2657        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2658        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2659        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2660        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2661        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2662
2663        tempOld = None  # pandas object for old history, if --only-missing key present
2664        lastTime = None  # datetime object of last old candle in file
2665
2666        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2667            uLogger.debug("--only-missing key present, add only last missing candles...")
2668            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2669
2670            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2671
2672            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2673            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2674            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2675            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2676
2677            # get last datetime object from last string in file or minus 1 delta if file is empty:
2678            if len(tempOld) > 0:
2679                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2680
2681            else:
2682                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2683
2684            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2685
2686        responseJSONs = []  # raw history blocks of data
2687
2688        blockEnd = dtEnd
2689        for item in range(blocks):
2690            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2691            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2692
2693            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2694                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2695            ))
2696
2697            if blockStart == blockEnd:
2698                uLogger.debug("Skipped this zero-length block...")
2699
2700            else:
2701                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2702                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2703                self.body = str({
2704                    "figi": self._figi,
2705                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2706                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2707                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2708                })
2709                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2710
2711                if "code" in responseJSON.keys():
2712                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2713
2714                else:
2715                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2716                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2717
2718                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2719
2720            blockEnd = blockStart
2721
2722        printCount = len(responseJSONs)  # candles to show in console
2723        if responseJSONs:
2724            tempHistory = pd.DataFrame(
2725                data={
2726                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2727                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2728                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2729                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2730                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2731                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2732                    "volume": [int(item["volume"]) for item in responseJSONs],
2733                },
2734                index=range(len(responseJSONs)),
2735                columns=["date", "time", "open", "high", "low", "close", "volume"],
2736            )
2737            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2738            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2739
2740            # append only newest candles to old history if --only-missing key present:
2741            if onlyMissing and tempOld is not None and lastTime is not None:
2742                index = 0  # find start index in tempHistory data:
2743
2744                for i, item in tempHistory.iterrows():
2745                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2746
2747                    if curTime == lastTime:
2748                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2749                        index = i
2750                        printCount = index + 1
2751                        break
2752
2753                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2754
2755            else:
2756                history = tempHistory  # if no `--only-missing` key then load full data from server
2757
2758            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2759
2760        if history is not None and not history.empty:
2761            if show:
2762                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2763                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2764                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2765                ))
2766
2767        else:
2768            uLogger.warning("Received an empty candles history!")
2769
2770        if self.historyFile is not None:
2771            if history is not None and not history.empty:
2772                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2773                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2774
2775            else:
2776                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2777
2778        else:
2779            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2780
2781        return history
2782
2783    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2784        """
2785        Load candles history from csv-file and return Pandas DataFrame object.
2786
2787        See also: `History()` and `ShowHistoryChart()` methods.
2788
2789        :param filePath: path to csv-file to open.
2790        """
2791        loadedHistory = None  # init candles data object
2792
2793        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2794
2795        if os.path.exists(filePath):
2796            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2797
2798            tfStr = self.priceModel.FormattedDelta(
2799                self.priceModel.timeframe,
2800                "{days} days {hours}h {minutes}m {seconds}s",
2801            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2802                self.priceModel.timeframe,
2803                "{hours}h {minutes}m {seconds}s",
2804            )
2805
2806            if loadedHistory is not None and not loadedHistory.empty:
2807                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2808                    len(loadedHistory),
2809                    tfStr,
2810                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2811                )
2812
2813            else:
2814                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2815
2816        else:
2817            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2818
2819        return loadedHistory
2820
2821    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2822        """
2823        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2824
2825        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2826        Default: `index.html` (both for interact and non-interact candlesticks chart).
2827
2828        See also: `History()` and `LoadHistory()` methods.
2829
2830        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2831        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2832                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2833                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2834                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2835        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2836                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2837        """
2838        if isinstance(candles, str):
2839            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2840            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2841
2842        elif isinstance(candles, pd.DataFrame):
2843            self.priceModel.prices = candles  # set candles chain from variable
2844            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2845
2846            if "datetime" not in candles.columns:
2847                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2848
2849        else:
2850            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2851            raise Exception("Incorrect value")
2852
2853        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2854
2855        if interact:
2856            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2857
2858            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2859
2860        else:
2861            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2862
2863            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2864
2865        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2866
2867    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2868        """
2869        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2870        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2871
2872        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2873
2874        :param operation: string "Buy" or "Sell".
2875        :param lots: volume, integer count of lots >= 1.
2876        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2877        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2878        :param expDate: string "Undefined" by default or local date in future,
2879                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2880        :return: JSON with response from broker server.
2881        """
2882        if self.accountId is None or not self.accountId:
2883            uLogger.error("Variable `accountId` must be defined for using this method!")
2884            raise Exception("Account ID required")
2885
2886        if operation is None or not operation or operation not in ("Buy", "Sell"):
2887            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2888            raise Exception("Incorrect value")
2889
2890        if lots is None or lots < 1:
2891            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2892            lots = 1
2893
2894        if tp is None or tp < 0:
2895            tp = 0
2896
2897        if sl is None or sl < 0:
2898            sl = 0
2899
2900        if expDate is None or not expDate:
2901            expDate = "Undefined"
2902
2903        if not (self._ticker or self._figi):
2904            uLogger.error("Ticker or FIGI must be defined!")
2905            raise Exception("Ticker or FIGI required")
2906
2907        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2908        self._ticker = instrument["ticker"]
2909        self._figi = instrument["figi"]
2910
2911        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
2912
2913        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2914        self.body = str({
2915            "figi": self._figi,
2916            "quantity": str(lots),
2917            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2918            "accountId": str(self.accountId),
2919            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2920        })
2921        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2922
2923        if "orderId" in response.keys():
2924            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2925                operation, response["orderId"],
2926                self._ticker, self._figi, lots,
2927                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2928                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2929                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2930            ))
2931
2932            if tp > 0:
2933                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2934
2935            if sl > 0:
2936                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2937
2938        else:
2939            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
2940
2941        return response
2942
2943    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2944        """
2945        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2946        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2947
2948        See also: `Order()` and `Trade()` docstrings.
2949
2950        :param lots: volume, integer count of lots >= 1.
2951        :param tp: float > 0, take profit price of stop-order.
2952        :param sl: float > 0, stop loss price of stop-order.
2953        :param expDate: it's a local date in future.
2954                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2955        :return: JSON with response from broker server.
2956        """
2957        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2958
2959    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2960        """
2961        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2962        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2963
2964        See also: `Order()` and `Trade()` docstrings.
2965
2966        :param lots: volume, integer count of lots >= 1.
2967        :param tp: float > 0, take profit price of stop-order.
2968        :param sl: float > 0, stop loss price of stop-order.
2969        :param expDate: it's a local date in the future.
2970                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2971        :return: JSON with response from broker server.
2972        """
2973        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
2974
2975    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
2976        """
2977        Close position of given instruments.
2978
2979        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
2980        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2981                         This avoids unnecessary downloading data from the server.
2982        """
2983        if instruments is None or not instruments:
2984            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
2985            raise Exception("Ticker or FIGI required")
2986
2987        if isinstance(instruments, str):
2988            instruments = [instruments]
2989
2990        uniqueInstruments = self.GetUniqueFIGIs(instruments)
2991        if uniqueInstruments:
2992            if portfolio is None or not portfolio:
2993                portfolio = self.Overview(show=False)
2994
2995            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
2996            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
2997
2998            for self._figi in uniqueInstruments:
2999                if self._figi not in allOpened:
3000                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3001                    continue
3002
3003                # search open trade info about instrument by ticker:
3004                instrument = {}
3005                for iType in TKS_INSTRUMENTS:
3006                    if instrument:
3007                        break
3008
3009                    for item in portfolio["stat"][iType]:
3010                        if item["figi"] == self._figi:
3011                            instrument = item
3012                            break
3013
3014                if instrument:
3015                    self._ticker = instrument["ticker"]
3016                    self._figi = instrument["figi"]
3017
3018                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3019                        self._ticker,
3020                        self._figi,
3021                        int(instrument["volume"]),
3022                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3023                    ))
3024
3025                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3026
3027                    if tradeLots > 0:
3028                        if instrument["blocked"] > 0:
3029                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3030                                instrument["blocked"],
3031                                self._ticker,
3032                                tradeLots,
3033                            ))
3034
3035                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3036                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3037
3038                    else:
3039                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3040
3041    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3042        """
3043        Close all positions of given instruments with defined type.
3044
3045        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3046        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3047                         This avoids unnecessary downloading data from the server.
3048        """
3049        if iType not in TKS_INSTRUMENTS:
3050            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3051
3052        else:
3053            if portfolio is None or not portfolio:
3054                portfolio = self.Overview(show=False)
3055
3056            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3057            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3058
3059            if tickers and portfolio:
3060                self.CloseTrades(tickers, portfolio)
3061
3062            else:
3063                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3064
3065    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3066        """
3067        Universal method to create market or limit orders with all available parameters for current `accountId`.
3068        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3069
3070        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3071        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3072
3073        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3074        then broker immediately open market order as you can do simple --buy or --sell operations!
3075
3076        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3077        When current price will go up or down to target price value then broker opens a limit order.
3078        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3079
3080        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3081
3082        :param operation: string "Buy" or "Sell".
3083        :param orderType: string "Limit" or "Stop".
3084        :param lots: volume, integer count of lots >= 1.
3085        :param targetPrice: target price > 0. This is open trade price for limit order.
3086        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3087                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3088        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3089                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3090                         Stop loss order always executed by market price.
3091        :param expDate: string "Undefined" by default or local date in future.
3092                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3093                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3094                        A limit order has no expiration date, it lasts until the end of the trading day.
3095        :return: JSON with response from broker server.
3096        """
3097        if self.accountId is None or not self.accountId:
3098            uLogger.error("Variable `accountId` must be defined for using this method!")
3099            raise Exception("Account ID required")
3100
3101        if operation is None or not operation or operation not in ("Buy", "Sell"):
3102            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3103            raise Exception("Incorrect value")
3104
3105        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3106            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3107            raise Exception("Incorrect value")
3108
3109        if lots is None or lots < 1:
3110            uLogger.error("You must define trade volume > 0: integer count of lots!")
3111            raise Exception("Incorrect value")
3112
3113        if targetPrice is None or targetPrice <= 0:
3114            uLogger.error("Target price for limit-order must be greater than 0!")
3115            raise Exception("Incorrect value")
3116
3117        if limitPrice is None or limitPrice <= 0:
3118            limitPrice = targetPrice
3119
3120        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3121            stopType = "Limit"
3122
3123        if expDate is None or not expDate:
3124            expDate = "Undefined"
3125
3126        if not (self._ticker or self._figi):
3127            uLogger.error("Tocker or FIGI must be defined!")
3128            raise Exception("Ticker or FIGI required")
3129
3130        response = {}
3131        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3132        self._ticker = instrument["ticker"]
3133        self._figi = instrument["figi"]
3134
3135        if orderType == "Limit":
3136            uLogger.debug(
3137                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3138                    self._ticker, self._figi,
3139                    operation, lots, targetPrice, instrument["currency"],
3140                ))
3141
3142            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3143            self.body = str({
3144                "figi": self._figi,
3145                "quantity": str(lots),
3146                "price": FloatToNano(targetPrice),
3147                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3148                "accountId": str(self.accountId),
3149                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3150            })
3151            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3152
3153            if "orderId" in response.keys():
3154                uLogger.info(
3155                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3156                        response["orderId"],
3157                        self._ticker, self._figi,
3158                        operation, lots, targetPrice, instrument["currency"],
3159                    ))
3160
3161                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3162                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3163                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3164                            targetPrice, instrument["currency"],
3165                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3166                        ))
3167
3168                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3169                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3170                            targetPrice, instrument["currency"],
3171                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3172                        ))
3173
3174            else:
3175                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3176
3177        if orderType == "Stop":
3178            uLogger.debug(
3179                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3180                    self._ticker, self._figi,
3181                    operation, lots,
3182                    targetPrice, instrument["currency"],
3183                    limitPrice, instrument["currency"],
3184                    stopType, expDate,
3185                ))
3186
3187            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3188            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3189            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3190
3191            body = {
3192                "figi": self._figi,
3193                "quantity": str(lots),
3194                "price": FloatToNano(limitPrice),
3195                "stopPrice": FloatToNano(targetPrice),
3196                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3197                "accountId": str(self.accountId),
3198                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3199                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3200            }
3201
3202            if expDateUTC:
3203                body["expireDate"] = expDateUTC
3204
3205            self.body = str(body)
3206            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3207
3208            if "stopOrderId" in response.keys():
3209                uLogger.info(
3210                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3211                        response["stopOrderId"],
3212                        self._ticker, self._figi,
3213                        operation, lots,
3214                        targetPrice, instrument["currency"],
3215                        limitPrice, instrument["currency"],
3216                        TKS_STOP_ORDER_TYPES[stopOrderType],
3217                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3218                    ))
3219
3220                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3221                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3222                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3223                            targetPrice, instrument["currency"],
3224                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3225                        ))
3226
3227                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3228                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3229                            targetPrice, instrument["currency"],
3230                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3231                        ))
3232
3233            else:
3234                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3235
3236        return response
3237
3238    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3239        """
3240        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3241        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3242        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3243        See also: `Order()` docstring.
3244
3245        :param lots: volume, integer count of lots >= 1.
3246        :param targetPrice: target price > 0. This is open trade price for limit order.
3247        :return: JSON with response from broker server.
3248        """
3249        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3250
3251    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3252        """
3253        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3254        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3255        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3256        target price value then broker opens a limit order. See also: `Order()` docstring.
3257
3258        :param lots: volume, integer count of lots >= 1.
3259        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3260        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3261                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3262        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3263                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3264        :param expDate: string "Undefined" by default or local date in future.
3265                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3266                        This date is converting to UTC format for server.
3267        :return: JSON with response from broker server.
3268        """
3269        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3270
3271    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3272        """
3273        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3274        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3275        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3276        See also: `Order()` docstring.
3277
3278        :param lots: volume, integer count of lots >= 1.
3279        :param targetPrice: target price > 0. This is open trade price for limit order.
3280        :return: JSON with response from broker server.
3281        """
3282        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3283
3284    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3285        """
3286        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3287        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3288        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3289        target price value then broker opens a limit order. See also: `Order()` docstring.
3290
3291        :param lots: volume, integer count of lots >= 1.
3292        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3293        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3294                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3295        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3296                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3297        :param expDate: string "Undefined" by default or local date in future.
3298                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3299                        This date is converting to UTC format for server.
3300        :return: JSON with response from broker server.
3301        """
3302        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3303
3304    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3305        """
3306        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3307
3308        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3309        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3310                             This avoids unnecessary downloading data from the server.
3311        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3312        """
3313        if self.accountId is None or not self.accountId:
3314            uLogger.error("Variable `accountId` must be defined for using this method!")
3315            raise Exception("Account ID required")
3316
3317        if orderIDs:
3318            if allOrdersIDs is None:
3319                rawOrders = self.RequestPendingOrders()
3320                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3321
3322            if allStopOrdersIDs is None:
3323                rawStopOrders = self.RequestStopOrders()
3324                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3325
3326            for orderID in orderIDs:
3327                idInPendingOrders = orderID in allOrdersIDs
3328                idInStopOrders = orderID in allStopOrdersIDs
3329
3330                if not (idInPendingOrders or idInStopOrders):
3331                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3332                    continue
3333
3334                else:
3335                    if idInPendingOrders:
3336                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3337
3338                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3339                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3340                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3341                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3342
3343                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3344                            if self.moreDebug:
3345                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3346
3347                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3348
3349                        else:
3350                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3351
3352                    elif idInStopOrders:
3353                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3354
3355                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3356                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3357                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3358                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3359
3360                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3361                            if self.moreDebug:
3362                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3363
3364                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3365
3366                        else:
3367                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3368
3369                    else:
3370                        continue
3371
3372    def CloseAllOrders(self) -> None:
3373        """
3374        Gets a list of open pending and stop orders and cancel it all.
3375        """
3376        rawOrders = self.RequestPendingOrders()
3377        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3378        lenOrders = len(allOrdersIDs)
3379
3380        rawStopOrders = self.RequestStopOrders()
3381        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3382        lenSOrders = len(allStopOrdersIDs)
3383
3384        if lenOrders > 0 or lenSOrders > 0:
3385            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3386
3387            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3388
3389        else:
3390            uLogger.info("Orders not found, nothing to cancel.")
3391
3392    def CloseAll(self, *args) -> None:
3393        """
3394        Close all available (not blocked) opened trades and orders.
3395
3396        Also, you can select one or more keywords case-insensitive:
3397        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3398
3399        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3400        """
3401        overview = self.Overview(show=False)  # get all open trades info
3402
3403        if len(args) == 0:
3404            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3405            self.CloseAllOrders()  # close all pending and stop orders
3406
3407            for iType in TKS_INSTRUMENTS:
3408                if iType != "Currencies":
3409                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3410
3411        else:
3412            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3413            lowerArgs = [x.lower() for x in args]
3414
3415            if "orders" in lowerArgs:
3416                self.CloseAllOrders()  # close all pending and stop orders
3417
3418            for iType in TKS_INSTRUMENTS:
3419                if iType.lower() in lowerArgs and iType != "Currencies":
3420                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3421
3422    def CloseAllByTicker(self, instrument: str) -> None:
3423        """
3424        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3425
3426        This method searches opened trade and orders of instrument throw all portfolio and then use
3427        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3428
3429        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3430
3431        :param instrument: string with ticker.
3432        """
3433        if instrument is None or not instrument:
3434            uLogger.error("Ticker name must be defined for using this method!")
3435            raise Exception("Ticker required")
3436
3437        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3438
3439        self._ticker = instrument  # try to set instrument as ticker
3440        self._figi = ""
3441
3442        if self.IsInPortfolio(portfolio=overview):
3443            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3444            self.CloseTrades(instruments=[instrument], portfolio=overview)
3445
3446        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3447        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3448
3449        if limitAll and self.IsInLimitOrders(portfolio=overview):
3450            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3451            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3452
3453        if stopAll and self.IsInStopOrders(portfolio=overview):
3454            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3455            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3456
3457    def CloseAllByFIGI(self, instrument: str) -> None:
3458        """
3459        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3460
3461        This method searches opened trade and orders of instrument throw all portfolio and then use
3462        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3463
3464        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3465
3466        :param instrument: string with FIGI id.
3467        """
3468        if instrument is None or not instrument:
3469            uLogger.error("FIGI id must be defined for using this method!")
3470            raise Exception("FIGI required")
3471
3472        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3473
3474        self._ticker = ""
3475        self._figi = instrument  # try to set instrument as FIGI id
3476
3477        if self.IsInPortfolio(portfolio=overview):
3478            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3479            self.CloseTrades(instruments=[instrument], portfolio=overview)
3480
3481        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3482        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3483
3484        if limitAll and self.IsInLimitOrders(portfolio=overview):
3485            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3486            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3487
3488        if stopAll and self.IsInStopOrders(portfolio=overview):
3489            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3490            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3491
3492    @staticmethod
3493    def ParseOrderParameters(operation, **inputParameters):
3494        """
3495        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3496
3497        :param operation: string "Buy" or "Sell".
3498        :param inputParameters: this is dict of strings that looks like this
3499               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3500               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3501               "prices" key: one or more prices to open limit-orders
3502               Counts of values in lots and prices lists must be equals!
3503        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3504        """
3505        # TODO: update order grid work with api v2
3506        pass
3507        # uLogger.debug("Input parameters: {}".format(inputParameters))
3508        #
3509        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3510        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3511        #     raise Exception("Incorrect value")
3512        #
3513        # if "l" in inputParameters.keys():
3514        #     inputParameters["lots"] = inputParameters.pop("l")
3515        #
3516        # if "p" in inputParameters.keys():
3517        #     inputParameters["prices"] = inputParameters.pop("p")
3518        #
3519        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3520        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3521        #     raise Exception("Incorrect value")
3522        #
3523        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3524        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3525        #
3526        # if len(lots) != len(prices):
3527        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3528        #     raise Exception("Incorrect value")
3529        #
3530        # uLogger.debug("Extracted parameters for orders:")
3531        # uLogger.debug("lots = {}".format(lots))
3532        # uLogger.debug("prices = {}".format(prices))
3533        #
3534        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3535        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3536        # uLogger.debug("Order parameters: {}".format(result))
3537        #
3538        # return result
3539
3540    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3541        """
3542        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3543
3544        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3545        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3546        """
3547        result = False
3548        msg = "Instrument not defined!"
3549
3550        if portfolio is None or not portfolio:
3551            portfolio = self.Overview(show=False)
3552
3553        if self._ticker:
3554            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3555            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3556
3557            for iType in TKS_INSTRUMENTS:
3558                for instrument in portfolio["stat"][iType]:
3559                    if instrument["ticker"] == self._ticker:
3560                        result = True
3561                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3562                        break
3563
3564        elif self._figi:
3565            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3566            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3567
3568            for iType in TKS_INSTRUMENTS:
3569                for instrument in portfolio["stat"][iType]:
3570                    if instrument["figi"] == self._figi:
3571                        result = True
3572                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3573                        break
3574
3575        else:
3576            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3577
3578        uLogger.debug(msg)
3579
3580        return result
3581
3582    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3583        """
3584        Returns instrument from the user's portfolio if it presents there.
3585        Instrument must be defined by `ticker` (highly priority) or `figi`.
3586
3587        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3588        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3589        """
3590        result = None
3591        msg = "Instrument not defined!"
3592
3593        if portfolio is None or not portfolio:
3594            portfolio = self.Overview(show=False)
3595
3596        if self._ticker:
3597            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3598            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3599
3600            for iType in TKS_INSTRUMENTS:
3601                for instrument in portfolio["stat"][iType]:
3602                    if instrument["ticker"] == self._ticker:
3603                        result = instrument
3604                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3605                        break
3606
3607        elif self._figi:
3608            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3609            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3610
3611            for iType in TKS_INSTRUMENTS:
3612                for instrument in portfolio["stat"][iType]:
3613                    if instrument["figi"] == self._figi:
3614                        result = instrument
3615                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3616                        break
3617
3618        else:
3619            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3620
3621        uLogger.debug(msg)
3622
3623        return result
3624
3625    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3626        """
3627        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3628
3629        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3630
3631        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3632        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3633        """
3634        result = False
3635        msg = "Instrument not defined!"
3636
3637        if portfolio is None or not portfolio:
3638            portfolio = self.Overview(show=False)
3639
3640        if self._ticker:
3641            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3642            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3643
3644            for instrument in portfolio["stat"]["orders"]:
3645                if instrument["ticker"] == self._ticker:
3646                    result = True
3647                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3648                    break
3649
3650        elif self._figi:
3651            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3652            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3653
3654            for instrument in portfolio["stat"]["orders"]:
3655                if instrument["figi"] == self._figi:
3656                    result = True
3657                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3658                    break
3659
3660        else:
3661            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3662
3663        uLogger.debug(msg)
3664
3665        return result
3666
3667    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3668        """
3669        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3670        Instrument must be defined by `ticker` (highly priority) or `figi`.
3671
3672        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3673
3674        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3675        :return: list with `orderID`s of limit orders.
3676        """
3677        result = []
3678        msg = "Instrument not defined!"
3679
3680        if portfolio is None or not portfolio:
3681            portfolio = self.Overview(show=False)
3682
3683        if self._ticker:
3684            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3685            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3686
3687            for instrument in portfolio["stat"]["orders"]:
3688                if instrument["ticker"] == self._ticker:
3689                    result.append(instrument["orderID"])
3690
3691            if result:
3692                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3693
3694        elif self._figi:
3695            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3696            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3697
3698            for instrument in portfolio["stat"]["orders"]:
3699                if instrument["figi"] == self._figi:
3700                    result.append(instrument["orderID"])
3701
3702            if result:
3703                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3704
3705        else:
3706            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3707
3708        uLogger.debug(msg)
3709
3710        return result
3711
3712    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3713        """
3714        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3715
3716        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3717
3718        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3719        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3720        """
3721        result = False
3722        msg = "Instrument not defined!"
3723
3724        if portfolio is None or not portfolio:
3725            portfolio = self.Overview(show=False)
3726
3727        if self._ticker:
3728            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3729            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3730
3731            for instrument in portfolio["stat"]["stopOrders"]:
3732                if instrument["ticker"] == self._ticker:
3733                    result = True
3734                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3735                    break
3736
3737        elif self._figi:
3738            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3739            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3740
3741            for instrument in portfolio["stat"]["stopOrders"]:
3742                if instrument["figi"] == self._figi:
3743                    result = True
3744                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3745                    break
3746
3747        else:
3748            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3749
3750        uLogger.debug(msg)
3751
3752        return result
3753
3754    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3755        """
3756        Returns list with all `orderID`s of opened stop orders for the instrument.
3757        Instrument must be defined by `ticker` (highly priority) or `figi`.
3758
3759        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3760
3761        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3762        :return: list with `orderID`s of stop orders.
3763        """
3764        result = []
3765        msg = "Instrument not defined!"
3766
3767        if portfolio is None or not portfolio:
3768            portfolio = self.Overview(show=False)
3769
3770        if self._ticker:
3771            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3772            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3773
3774            for instrument in portfolio["stat"]["stopOrders"]:
3775                if instrument["ticker"] == self._ticker:
3776                    result.append(instrument["orderID"])
3777
3778            if result:
3779                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3780
3781        elif self._figi:
3782            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3783            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3784
3785            for instrument in portfolio["stat"]["stopOrders"]:
3786                if instrument["figi"] == self._figi:
3787                    result.append(instrument["orderID"])
3788
3789            if result:
3790                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3791
3792        else:
3793            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3794
3795        uLogger.debug(msg)
3796
3797        return result
3798
3799    def RequestLimits(self) -> dict:
3800        """
3801        Method for obtaining the available funds for withdrawal for current `accountId`.
3802
3803        See also:
3804        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3805        - `OverviewLimits()` method
3806
3807        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3808                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3809                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3810                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3811        """
3812        if self.accountId is None or not self.accountId:
3813            uLogger.error("Variable `accountId` must be defined for using this method!")
3814            raise Exception("Account ID required")
3815
3816        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3817
3818        self.body = str({"accountId": self.accountId})
3819        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3820        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3821
3822        if self.moreDebug:
3823            uLogger.debug("Records about available funds for withdrawal successfully received")
3824
3825        return rawLimits
3826
3827    def OverviewLimits(self, show: bool = False) -> dict:
3828        """
3829        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3830
3831        See also: `RequestLimits()`.
3832
3833        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3834        :return: dict with raw parsed data from server and some calculated statistics about it.
3835        """
3836        if self.accountId is None or not self.accountId:
3837            uLogger.error("Variable `accountId` must be defined for using this method!")
3838            raise Exception("Account ID required")
3839
3840        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3841
3842        view = {
3843            "rawLimits": rawLimits,
3844            "limits": {  # parsed data for every currency:
3845                "money": {  # this is an array of portfolio currency positions
3846                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3847                },
3848                "blocked": {  # this is an array of blocked currency
3849                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3850                },
3851                "blockedGuarantee": {  # this is locked money under collateral for futures
3852                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3853                },
3854            },
3855        }
3856
3857        # --- Prepare text table with limits in human-readable format:
3858        if show:
3859            info = [
3860                "# Withdrawal limits\n\n",
3861                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3862                "* **Account ID:** [{}]\n".format(self.accountId),
3863            ]
3864
3865            if view["limits"]["money"]:
3866                info.extend([
3867                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3868                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3869                ])
3870
3871            else:
3872                info.append("\nNo withdrawal limits\n")
3873
3874            for curr in view["limits"]["money"].keys():
3875                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3876                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3877                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3878
3879                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3880                    "[{}]".format(curr),
3881                    "{:.2f}".format(view["limits"]["money"][curr]),
3882                    "{:.2f}".format(availableMoney),
3883                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3884                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3885                )
3886
3887                if curr == "rub":
3888                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3889
3890                else:
3891                    info.append(infoStr)
3892
3893            infoText = "".join(info)
3894
3895            uLogger.info(infoText)
3896
3897            if self.withdrawalLimitsFile:
3898                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3899                    fH.write(infoText)
3900
3901                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3902
3903        return view
3904
3905    def RequestAccounts(self) -> dict:
3906        """
3907        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3908
3909        See also:
3910        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3911        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3912        - `OverviewUserInfo()` method
3913
3914        :return: dict with raw data from server that contains accounts info. Example of dict:
3915                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3916                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3917                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3918                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3919        """
3920        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3921
3922        self.body = str({})
3923        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3924        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3925
3926        if self.moreDebug:
3927            uLogger.debug("Records about available accounts successfully received")
3928
3929        return rawAccounts
3930
3931    def RequestUserInfo(self) -> dict:
3932        """
3933        Method for requesting common user's information.
3934
3935        See also:
3936        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3937        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3938        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3939        - `OverviewUserInfo()` method
3940
3941        :return: dict with raw data from server that contains user's information. Example of dict:
3942                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3943                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3944        """
3945        uLogger.debug("Requesting common user's information. Wait, please...")
3946
3947        self.body = str({})
3948        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3949        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3950
3951        if self.moreDebug:
3952            uLogger.debug("Records about current user successfully received")
3953
3954        return rawUserInfo
3955
3956    def RequestMarginStatus(self, accountId: str = None) -> dict:
3957        """
3958        Method for requesting margin calculation for defined account ID.
3959
3960        See also:
3961        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3962        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3963        - `OverviewUserInfo()` method
3964
3965        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3966        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3967                 Example of responses:
3968                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3969                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3970                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3971                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3972                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3973                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3974        """
3975        if accountId is None or not accountId:
3976            if self.accountId is None or not self.accountId:
3977                uLogger.error("Variable `accountId` must be defined for using this method!")
3978                raise Exception("Account ID required")
3979
3980            else:
3981                accountId = self.accountId  # use `self.accountId` (main ID) by default
3982
3983        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3984
3985        self.body = str({"accountId": accountId})
3986        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3987        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3988
3989        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3990            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3991            rawMargin = {}
3992
3993        else:
3994            if self.moreDebug:
3995                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3996
3997        return rawMargin
3998
3999    def RequestTariffLimits(self) -> dict:
4000        """
4001        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4002
4003        See also:
4004        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4005        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4006        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4007        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4008        - `OverviewUserInfo()` method
4009
4010        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4011                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4012                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4013        """
4014        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4015
4016        self.body = str({})
4017        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4018        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4019
4020        if self.moreDebug:
4021            uLogger.debug("Records with limits of current tariff successfully received")
4022
4023        return rawTariffLimits
4024
4025    def RequestBondCoupons(self, iJSON: dict) -> dict:
4026        """
4027        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4028        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4029        All dates are in UTC timezone.
4030
4031        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4032        Documentation:
4033        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4034        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4035
4036        See also: `ExtendBondsData()`.
4037
4038        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4039                      If raw iJSON is not data of bond then server returns an error [400] with message:
4040                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4041        :return: dictionary with bond payment calendar. Response example
4042                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4043                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4044                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4045                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4046        """
4047        if iJSON["figi"] is None or not iJSON["figi"]:
4048            uLogger.error("FIGI must be defined for using this method!")
4049            raise Exception("FIGI required")
4050
4051        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4052        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4053
4054        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4055            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4056            self._figi,
4057            startDate,
4058            endDate,
4059        ))
4060
4061        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4062        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4063        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4064
4065        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4066            uLogger.warning("Instrument type is not bond!")
4067
4068        else:
4069            if self.moreDebug:
4070                uLogger.debug("Records about bond payment calendar successfully received")
4071
4072        return calendar
4073
4074    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4075        """
4076        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4077        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4078        coupon yields, current yields and some statistics etc.
4079
4080        WARNING! This is too long operation if a lot of bonds requested from broker server.
4081
4082        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4083
4084        :param instruments: list of strings with tickers or FIGIs.
4085        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4086                     for further used by data scientists or stock analytics.
4087        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4088                 In XLSX-file and Pandas DataFrame fields mean:
4089                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4090                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4091        """
4092        if instruments is None or not instruments:
4093            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4094            raise Exception("Ticker or FIGI required")
4095
4096        if isinstance(instruments, str):
4097            instruments = [instruments]
4098
4099        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4100
4101        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4102
4103        iCount = len(uniqueInstruments)
4104        tooLong = iCount >= 20
4105        if tooLong:
4106            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4107
4108        bonds = None
4109        for i, self._figi in enumerate(uniqueInstruments):
4110            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4111
4112            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4113                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4114                rawBond = self.SearchByFIGI(requestPrice=True)
4115
4116                # Widen raw data with UTC current time (iData["actualDateTime"]):
4117                actualDate = datetime.now(tzutc())
4118                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4119
4120                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4121                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4122
4123                # Replace some values with human-readable:
4124                iData["nominalCurrency"] = iData["nominal"]["currency"]
4125                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4126                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4127                iData["aciCurrency"] = iData["aciValue"]["currency"]
4128                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4129                iData["issueSize"] = int(iData["issueSize"])
4130                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4131                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4132                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4133                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4134                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4135                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4136                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4137                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4138                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4139                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4140
4141                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4142                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4143                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4144                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4145                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4146                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4147                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4148                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4149                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4150                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4151                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4152
4153                # Widen raw data with calendar data from `rawCalendar` values:
4154                calendarData = []
4155                if "events" in iData["rawCalendar"].keys():
4156                    for item in iData["rawCalendar"]["events"]:
4157                        calendarData.append({
4158                            "couponDate": item["couponDate"],
4159                            "couponNumber": int(item["couponNumber"]),
4160                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4161                            "payCurrency": item["payOneBond"]["currency"],
4162                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4163                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4164                            "couponStartDate": item["couponStartDate"],
4165                            "couponEndDate": item["couponEndDate"],
4166                            "couponPeriod": item["couponPeriod"],
4167                        })
4168
4169                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4170                    if "maturityDate" not in iData.keys():
4171                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4172
4173                # Widen raw data with Coupon Rate.
4174                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4175                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4176                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4177                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4178
4179                # Widen raw data with Yield to Maturity (YTM) on current date.
4180                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4181                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4182                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4183                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4184                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4185                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4186
4187                iData["calendar"] = calendarData  # adds calendar at the end
4188
4189                # Remove not used data:
4190                iData.pop("uid")
4191                iData.pop("positionUid")
4192                iData.pop("currentPrice")
4193                iData.pop("rawCalendar")
4194
4195                colNames = list(iData.keys())
4196                if bonds is None:
4197                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4198
4199                else:
4200                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4201
4202            else:
4203                uLogger.warning("Instrument is not a bond!")
4204
4205            processed = round(100 * (i + 1) / iCount, 1)
4206            if tooLong and processed % 5 == 0:
4207                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4208
4209            else:
4210                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4211
4212        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4213
4214        # Saving bonds from Pandas DataFrame to XLSX sheet:
4215        if xlsx and self.bondsXLSXFile:
4216            with pd.ExcelWriter(
4217                    path=self.bondsXLSXFile,
4218                    date_format=TKS_DATE_FORMAT,
4219                    datetime_format=TKS_DATE_TIME_FORMAT,
4220                    mode="w",
4221            ) as writer:
4222                bonds.to_excel(
4223                    writer,
4224                    sheet_name="Extended bonds data",
4225                    index=True,
4226                    encoding="UTF-8",
4227                    freeze_panes=(1, 1),
4228                )  # saving as XLSX-file with freeze first row and column as headers
4229
4230            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4231
4232        return bonds
4233
4234    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4235        """
4236        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4237
4238        WARNING! This is too long operation if a lot of bonds requested from broker server.
4239
4240        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4241
4242        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4243                        extended information about bonds: main info, current prices, bond payment calendar,
4244                        coupon yields, current yields and some statistics etc.
4245                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4246        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4247                     for further used by data scientists or stock analytics.
4248        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4249        """
4250        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4251            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4252
4253        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4254
4255        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4256        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4257        calendar = None
4258        for bond in extBonds.iterrows():
4259            for item in bond[1]["calendar"]:
4260                cData = {
4261                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4262                    "couponDate": item["couponDate"],
4263                    "figi": bond[1]["figi"],
4264                    "ticker": bond[1]["ticker"],
4265                    "name": bond[1]["name"],
4266                    "couponNumber": item["couponNumber"],
4267                    "payOneBond": item["payOneBond"],
4268                    "payCurrency": item["payCurrency"],
4269                    "couponType": item["couponType"],
4270                    "couponPeriod": item["couponPeriod"],
4271                    "fixDate": item["fixDate"],
4272                    "couponStartDate": item["couponStartDate"],
4273                    "couponEndDate": item["couponEndDate"],
4274                }
4275
4276                if calendar is None:
4277                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4278
4279                else:
4280                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4281
4282        if calendar is not None:
4283            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4284
4285            # Saving calendar from Pandas DataFrame to XLSX sheet:
4286            if xlsx:
4287                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4288
4289                with pd.ExcelWriter(
4290                        path=xlsxCalendarFile,
4291                        date_format=TKS_DATE_FORMAT,
4292                        datetime_format=TKS_DATE_TIME_FORMAT,
4293                        mode="w",
4294                ) as writer:
4295                    humanReadable = calendar.copy(deep=True)
4296                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4297                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4298                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4299                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4300                    humanReadable.columns = colNames  # human-readable column names
4301
4302                    humanReadable.to_excel(
4303                        writer,
4304                        sheet_name="Bond payments calendar",
4305                        index=False,
4306                        encoding="UTF-8",
4307                        freeze_panes=(1, 2),
4308                    )  # saving as XLSX-file with freeze first row and column as headers
4309
4310                    del humanReadable  # release df in memory
4311
4312                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4313
4314        return calendar
4315
4316    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4317        """
4318        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4319        Also, creates Markdown file with calendar data, `calendar.md` by default.
4320
4321        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4322
4323        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4324                        extended information about bonds: main info, current prices, bond payment calendar,
4325                        coupon yields, current yields and some statistics etc.
4326                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4327        :param show: if `True` then also printing bonds payment calendar to the console,
4328                     otherwise save to file `calendarFile` only. `False` by default.
4329        :return: multilines text in Markdown format with bonds payment calendar as a table.
4330        """
4331        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4332            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4333
4334        infoText = "# Bond payments calendar\n\n"
4335
4336        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4337
4338        if not (calendar is None or calendar.empty):
4339            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4340
4341            info = [
4342                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4343                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4344            ]
4345
4346            newMonth = False
4347            notOneBond = calendar["figi"].nunique() > 1
4348            for i, bond in enumerate(calendar.iterrows()):
4349                if newMonth and notOneBond:
4350                    info.append(splitLine)
4351
4352                info.append(
4353                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4354                        "  √" if bond[1]["paid"] else "  —",
4355                        bond[1]["couponDate"].split("T")[0],
4356                        bond[1]["figi"],
4357                        bond[1]["ticker"],
4358                        bond[1]["couponNumber"],
4359                        "{} {}".format(
4360                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4361                            bond[1]["payCurrency"],
4362                        ),
4363                        bond[1]["couponType"],
4364                        bond[1]["couponPeriod"],
4365                        bond[1]["fixDate"].split("T")[0],
4366                    )
4367                )
4368
4369                if i < len(calendar.values) - 1:
4370                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4371                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4372                    newMonth = False if curDate.month == nextDate.month else True
4373
4374                else:
4375                    newMonth = False
4376
4377            infoText += "".join(info)
4378
4379            if show:
4380                uLogger.info("{}".format(infoText))
4381
4382            if self.calendarFile is not None:
4383                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4384                    fH.write(infoText)
4385
4386                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4387
4388        else:
4389            infoText += "No data\n"
4390
4391        return infoText
4392
4393    def OverviewAccounts(self, show: bool = False) -> dict:
4394        """
4395        Method for parsing and show simple table with all available user accounts.
4396
4397        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4398
4399        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4400        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4401                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4402                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4403                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4404                                                        "closed": "—", "access": "Full access" }, ...}}`
4405        """
4406        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4407
4408        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4409        accounts = {
4410            item["id"]: {
4411                "type": TKS_ACCOUNT_TYPES[item["type"]],
4412                "name": item["name"],
4413                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4414                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4415                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4416                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4417            } for item in rawAccounts["accounts"]
4418        }
4419
4420        # Raw and parsed data with some fields replaced in "stat" section:
4421        view = {
4422            "rawAccounts": rawAccounts,
4423            "stat": accounts,
4424        }
4425
4426        # --- Prepare simple text table with only accounts data in human-readable format:
4427        if show:
4428            info = [
4429                "# User accounts\n\n",
4430                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4431                "| Account ID   | Type                      | Status                    | Name                           |\n",
4432                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4433            ]
4434
4435            for account in view["stat"].keys():
4436                info.extend([
4437                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4438                        account,
4439                        view["stat"][account]["type"],
4440                        view["stat"][account]["status"],
4441                        view["stat"][account]["name"],
4442                    )
4443                ])
4444
4445            infoText = "".join(info)
4446
4447            uLogger.info(infoText)
4448
4449            if self.userAccountsFile:
4450                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4451                    fH.write(infoText)
4452
4453                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4454
4455        return view
4456
4457    def OverviewUserInfo(self, show: bool = False) -> dict:
4458        """
4459        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4460
4461        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4462
4463        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4464        :return: dict with raw parsed data from server and some calculated statistics about it.
4465        """
4466        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4467        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4468        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4469        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4470        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4471        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4472
4473        # This is dict with parsed common user data:
4474        userInfo = {
4475            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4476            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4477            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4478            "tariff": rawUserInfo["tariff"],
4479        }
4480
4481        # This is an array of dict with parsed margin statuses for every account IDs:
4482        margins = {}
4483        for accountId in accounts.keys():
4484            if rawMargins[accountId]:
4485                margins[accountId] = {
4486                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4487                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4488                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4489                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4490                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4491                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4492                }
4493
4494            else:
4495                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4496
4497        unary = {}  # unary-connection limits
4498        for item in rawTariffLimits["unaryLimits"]:
4499            if item["limitPerMinute"] in unary.keys():
4500                unary[item["limitPerMinute"]].extend(item["methods"])
4501
4502            else:
4503                unary[item["limitPerMinute"]] = item["methods"]
4504
4505        stream = {}  # stream-connection limits
4506        for item in rawTariffLimits["streamLimits"]:
4507            if item["limit"] in stream.keys():
4508                stream[item["limit"]].extend(item["streams"])
4509
4510            else:
4511                stream[item["limit"]] = item["streams"]
4512
4513        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4514        limits = {
4515            "unary": unary,
4516            "stream": stream,
4517        }
4518
4519        # Raw and parsed data as an output result:
4520        view = {
4521            "rawUserInfo": rawUserInfo,
4522            "rawAccounts": rawAccounts,
4523            "rawMargins": rawMargins,
4524            "rawTariffLimits": rawTariffLimits,
4525            "stat": {
4526                "userInfo": userInfo,
4527                "accounts": accounts,
4528                "margins": margins,
4529                "limits": limits,
4530            },
4531        }
4532
4533        # --- Prepare text table with user information in human-readable format:
4534        if show:
4535            info = [
4536                "# Full user information\n\n",
4537                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4538                "## Common information\n\n",
4539                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4540                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4541                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4542                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4543                "\n## User accounts\n\n",
4544            ]
4545
4546            for account in view["stat"]["accounts"].keys():
4547                info.extend([
4548                    "### ID: [{}]\n\n".format(account),
4549                    "| Parameters           | Values                                                       |\n",
4550                    "|----------------------|--------------------------------------------------------------|\n",
4551                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4552                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4553                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4554                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4555                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4556                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4557                ])
4558
4559                if margins[account]:
4560                    info.extend([
4561                        "| Margin status:       | Enabled                                                      |\n",
4562                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4563                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4564                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4565                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4566                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4567                    ])
4568
4569                else:
4570                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4571
4572            info.extend([
4573                "\n## Current user tariff limits\n",
4574                "\nSee also:\n",
4575                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4576                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4577                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4578                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4579                "\n### Unary limits\n",
4580            ])
4581
4582            if unary:
4583                for key, values in sorted(unary.items()):
4584                    info.append("\n* Max requests per minute: {}\n".format(key))
4585
4586                    for value in values:
4587                        info.append("  - {}\n".format(value))
4588
4589            else:
4590                info.append("\nNot available\n")
4591
4592            info.append("\n### Stream limits\n")
4593
4594            if stream:
4595                for key, values in sorted(stream.items()):
4596                    info.append("\n* Max stream connections: {}\n".format(key))
4597
4598                    for value in values:
4599                        info.append("  - {}\n".format(value))
4600
4601            else:
4602                info.append("\nNot available\n")
4603
4604            infoText = "".join(info)
4605
4606            uLogger.info(infoText)
4607
4608            if self.userInfoFile:
4609                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4610                    fH.write(infoText)
4611
4612                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4613
4614        return view
4615
4616
4617class Args:
4618    """
4619    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4620    """
4621    def __init__(self, **kwargs):
4622        self.__dict__.update(kwargs)
4623
4624    def __getattr__(self, item):
4625        return None
4626
4627
4628def ParseArgs():
4629    """This function get and parse command line keys."""
4630    parser = ArgumentParser()  # command-line string parser
4631
4632    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4633    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4634
4635    # --- options:
4636
4637    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4638    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4639    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4640
4641    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4642    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4643
4644    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4645    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4646
4647    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4648
4649    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4650    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4651    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4652
4653    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4654    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4655
4656    # --- commands:
4657
4658    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4659
4660    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4661    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4662    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4663    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4664    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4665    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4666    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4667    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4668
4669    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4670    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4671    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4672    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4673    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4674    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4675
4676    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4677    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4678    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4679    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4680
4681    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4682    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4683    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4684
4685    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4686    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4687    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4688    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4689    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4690    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4691    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4692
4693    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4694    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4695    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4696    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4697    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4698
4699    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4700    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4701    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4702
4703    cmdArgs = parser.parse_args()
4704    return cmdArgs
4705
4706
4707def Main(**kwargs):
4708    """
4709    Main function for work with TKSBrokerAPI in the console.
4710
4711    See examples:
4712    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4713    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4714    """
4715    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4716
4717    if args.debug_level:
4718        uLogger.level = 10  # always debug level by default
4719        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4720
4721    exitCode = 0
4722    start = datetime.now(tzutc())
4723    uLogger.debug("=-" * 50)
4724    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4725        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4726        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4727    ))
4728
4729    # trying to calculate full current version:
4730    buildVersion = __version__
4731    try:
4732        v = version("tksbrokerapi")
4733        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4734
4735    except Exception:
4736        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4737
4738    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4739    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4740
4741    try:
4742        if args.version:
4743            print("TKSBrokerAPI {}".format(buildVersion))
4744            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4745
4746        else:
4747            # Init class for trading with Tinkoff Broker:
4748            trader = TinkoffBrokerServer(
4749                token=args.token,
4750                accountId=args.account_id,
4751                useCache=not args.no_cache,
4752            )
4753
4754            # --- set some options:
4755
4756            if args.more:
4757                trader.moreDebug = True
4758                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4759
4760            if args.ticker:
4761                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4762
4763                if ticker in trader.aliasesKeys:
4764                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4765
4766                else:
4767                    trader.ticker = ticker
4768
4769            if args.figi:
4770                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4771
4772            if args.depth is not None:
4773                trader.depth = args.depth
4774
4775            # --- do one command:
4776
4777            if args.list:
4778                if args.output is not None:
4779                    trader.instrumentsFile = args.output
4780
4781                trader.ShowInstrumentsInfo(show=True)
4782
4783            elif args.list_xlsx:
4784                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4785
4786            elif args.bonds_xlsx is not None:
4787                if args.output is not None:
4788                    trader.bondsXLSXFile = args.output
4789
4790                if len(args.bonds_xlsx) == 0:
4791                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4792
4793                else:
4794                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4795
4796            elif args.search:
4797                if args.output is not None:
4798                    trader.searchResultsFile = args.output
4799
4800                trader.SearchInstruments(pattern=args.search[0], show=True)
4801
4802            elif args.info:
4803                if not (args.ticker or args.figi):
4804                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4805                    raise Exception("Ticker or FIGI required")
4806
4807                if args.output is not None:
4808                    trader.infoFile = args.output
4809
4810                if args.ticker:
4811                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4812
4813                else:
4814                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4815
4816            elif args.calendar is not None:
4817                if args.output is not None:
4818                    trader.calendarFile = args.output
4819
4820                if len(args.calendar) == 0:
4821                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4822
4823                else:
4824                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4825
4826                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4827
4828            elif args.price:
4829                if not (args.ticker or args.figi):
4830                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4831                    raise Exception("Ticker or FIGI required")
4832
4833                trader.GetCurrentPrices(show=True)
4834
4835            elif args.prices is not None:
4836                if args.output is not None:
4837                    trader.pricesFile = args.output
4838
4839                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4840
4841            elif args.overview:
4842                if args.output is not None:
4843                    trader.overviewFile = args.output
4844
4845                trader.Overview(show=True, details="full")
4846
4847            elif args.overview_digest:
4848                if args.output is not None:
4849                    trader.overviewDigestFile = args.output
4850
4851                trader.Overview(show=True, details="digest")
4852
4853            elif args.overview_positions:
4854                if args.output is not None:
4855                    trader.overviewPositionsFile = args.output
4856
4857                trader.Overview(show=True, details="positions")
4858
4859            elif args.overview_orders:
4860                if args.output is not None:
4861                    trader.overviewOrdersFile = args.output
4862
4863                trader.Overview(show=True, details="orders")
4864
4865            elif args.overview_analytics:
4866                if args.output is not None:
4867                    trader.overviewAnalyticsFile = args.output
4868
4869                trader.Overview(show=True, details="analytics")
4870
4871            elif args.overview_calendar:
4872                if args.output is not None:
4873                    trader.overviewAnalyticsFile = args.output
4874
4875                trader.Overview(show=True, details="calendar")
4876
4877            elif args.deals is not None:
4878                if args.output is not None:
4879                    trader.reportFile = args.output
4880
4881                if 0 <= len(args.deals) < 3:
4882                    trader.Deals(
4883                        start=args.deals[0] if len(args.deals) >= 1 else None,
4884                        end=args.deals[1] if len(args.deals) == 2 else None,
4885                        show=True,  # Always show deals report in console
4886                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4887                    )
4888
4889                else:
4890                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4891                    raise Exception("Incorrect value")
4892
4893            elif args.history is not None:
4894                if args.output is not None:
4895                    trader.historyFile = args.output
4896
4897                if 0 <= len(args.history) < 3:
4898                    dataReceived = trader.History(
4899                        start=args.history[0] if len(args.history) >= 1 else None,
4900                        end=args.history[1] if len(args.history) == 2 else None,
4901                        interval="hour" if args.interval is None or not args.interval else args.interval,
4902                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4903                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4904                        show=True,  # shows all downloaded candles in console
4905                    )
4906
4907                    if args.render_chart is not None and dataReceived is not None:
4908                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4909
4910                        trader.ShowHistoryChart(
4911                            candles=dataReceived,
4912                            interact=iChart,
4913                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4914                        )
4915
4916                else:
4917                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4918                    raise Exception("Incorrect value")
4919
4920            elif args.load_history is not None:
4921                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4922
4923                if args.render_chart is not None and histData is not None:
4924                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4925                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4926
4927                    trader.ShowHistoryChart(
4928                        candles=histData,
4929                        interact=iChart,
4930                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4931                    )
4932
4933            elif args.trade is not None:
4934                if 1 <= len(args.trade) <= 5:
4935                    trader.Trade(
4936                        operation=args.trade[0],
4937                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4938                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4939                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4940                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4941                    )
4942
4943                else:
4944                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4945
4946            elif args.buy is not None:
4947                if 0 <= len(args.buy) <= 4:
4948                    trader.Buy(
4949                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4950                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4951                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4952                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4953                    )
4954
4955                else:
4956                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4957
4958            elif args.sell is not None:
4959                if 0 <= len(args.sell) <= 4:
4960                    trader.Sell(
4961                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4962                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4963                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4964                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4965                    )
4966
4967                else:
4968                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4969
4970            elif args.order:
4971                if 4 <= len(args.order) <= 7:
4972                    trader.Order(
4973                        operation=args.order[0],
4974                        orderType=args.order[1],
4975                        lots=int(args.order[2]),
4976                        targetPrice=float(args.order[3]),
4977                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4978                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4979                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4980                    )
4981
4982                else:
4983                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4984
4985            elif args.buy_limit:
4986                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4987
4988            elif args.sell_limit:
4989                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4990
4991            elif args.buy_stop:
4992                if 2 <= len(args.buy_stop) <= 7:
4993                    trader.BuyStop(
4994                        lots=int(args.buy_stop[0]),
4995                        targetPrice=float(args.buy_stop[1]),
4996                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4997                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4998                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4999                    )
5000
5001                else:
5002                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5003
5004            elif args.sell_stop:
5005                if 2 <= len(args.sell_stop) <= 7:
5006                    trader.SellStop(
5007                        lots=int(args.sell_stop[0]),
5008                        targetPrice=float(args.sell_stop[1]),
5009                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5010                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5011                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5012                    )
5013
5014                else:
5015                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5016
5017            # elif args.buy_order_grid is not None:
5018            #     # update order grid work with api v2
5019            #     if len(args.buy_order_grid) == 2:
5020            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5021            #
5022            #         for order in orderParams:
5023            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5024            #
5025            #     else:
5026            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5027            #
5028            # elif args.sell_order_grid is not None:
5029            #     # update order grid work with api v2
5030            #     if len(args.sell_order_grid) >= 2:
5031            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5032            #
5033            #         for order in orderParams:
5034            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5035            #
5036            #     else:
5037            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5038
5039            elif args.close_order is not None:
5040                trader.CloseOrders(args.close_order)  # close only one order
5041
5042            elif args.close_orders is not None:
5043                trader.CloseOrders(args.close_orders)  # close list of orders
5044
5045            elif args.close_trade:
5046                if not (args.ticker or args.figi):
5047                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5048                    raise Exception("Ticker or FIGI required")
5049
5050                if args.ticker:
5051                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5052
5053                else:
5054                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5055
5056            elif args.close_trades is not None:
5057                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5058
5059            elif args.close_all is not None:
5060                if args.ticker:
5061                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5062
5063                elif args.figi:
5064                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5065
5066                else:
5067                    trader.CloseAll(*args.close_all)
5068
5069            elif args.limits:
5070                if args.output is not None:
5071                    trader.withdrawalLimitsFile = args.output
5072
5073                trader.OverviewLimits(show=True)
5074
5075            elif args.user_info:
5076                if args.output is not None:
5077                    trader.userInfoFile = args.output
5078
5079                trader.OverviewUserInfo(show=True)
5080
5081            elif args.account:
5082                if args.output is not None:
5083                    trader.userAccountsFile = args.output
5084
5085                trader.OverviewAccounts(show=True)
5086
5087            else:
5088                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5089                raise Exception("There is no command to execute")
5090
5091    except Exception:
5092        trace = tb.format_exc()
5093        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5094            if e in trace:
5095                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5096                break
5097
5098        uLogger.debug(trace)
5099        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5100        exitCode = 255  # an error occurred, must be open a ticket for this issue
5101
5102    finally:
5103        finish = datetime.now(tzutc())
5104
5105        if exitCode == 0:
5106            if args.more:
5107                uLogger.debug("All operations were finished success (summary code is 0).")
5108
5109        else:
5110            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5111                os.path.abspath(uLog.defaultLogFile), exitCode,
5112            ))
5113
5114        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5115        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5116            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5117            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5118        ))
5119        uLogger.debug("=-" * 50)
5120
5121        if not kwargs:
5122            sys.exit(exitCode)
5123
5124        else:
5125            return exitCode
5126
5127
5128if __name__ == "__main__":
5129    Main()
class TinkoffBrokerServer:
  76class TinkoffBrokerServer:
  77    """
  78    This class implements methods to work with Tinkoff broker server.
  79
  80    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  81
  82    About `token`: https://tinkoff.github.io/investAPI/token/
  83    """
  84    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  85        """
  86        Main class init.
  87
  88        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  89        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  90                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  91        :param useCache: use default cache file with raw data to use instead of `iList`.
  92                         True by default. Cache is auto-update if new day has come.
  93                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  94        :param defaultCache: path to default cache file. `dump.json` by default.
  95        """
  96        if token is None or not token:
  97            try:
  98                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
  99                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 100
 101            except KeyError:
 102                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 103                raise Exception("Token required")
 104
 105        else:
 106            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 107            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 108
 109        if accountId is None or not accountId:
 110            try:
 111                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 112                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 113
 114            except KeyError:
 115                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 116
 117        else:
 118            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 119            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 120
 121        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 122        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 123
 124        Latest version: https://pypi.org/project/tksbrokerapi/
 125        """
 126
 127        self.aliases = TKS_TICKER_ALIASES
 128        """Some aliases instead official tickers.
 129
 130        See also: `TKSEnums.TKS_TICKER_ALIASES`
 131        """
 132
 133        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 134
 135        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 136
 137        self._ticker = ""
 138        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 139
 140        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 141        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 142
 143        See also: `SearchByTicker()`, `SearchInstruments()`.
 144        """
 145
 146        self._figi = ""
 147        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 148
 149        See also: `SearchByFIGI()`, `SearchInstruments()`.
 150        """
 151
 152        self.depth = 1
 153        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 154
 155        See also: `GetCurrentPrices()`.
 156        """
 157
 158        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 159        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 160
 161        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 162        """
 163
 164        uLogger.debug("Broker API server: {}".format(self.server))
 165
 166        self.timeout = 15
 167        """Server operations timeout in seconds. Default: `15`.
 168
 169        See also: `SendAPIRequest()`.
 170        """
 171
 172        self.headers = {
 173            "Content-Type": "application/json",
 174            "accept": "application/json",
 175            "Authorization": "Bearer {}".format(self.token),
 176            "x-app-name": "Tim55667757.TKSBrokerAPI",
 177        }
 178        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 179
 180        See also: `SendAPIRequest()`.
 181        """
 182
 183        self.body = None
 184        """Request body which send to broker server. Default: `None`.
 185
 186        See also: `SendAPIRequest()`.
 187        """
 188
 189        self.moreDebug = False
 190        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 191
 192        self.historyFile = None
 193        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 194
 195        See also: `History()`.
 196        """
 197
 198        self.htmlHistoryFile = "index.html"
 199        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 200
 201        See also: `ShowHistoryChart()`.
 202        """
 203
 204        self.instrumentsFile = "instruments.md"
 205        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 206
 207        See also: `ShowInstrumentsInfo()`.
 208        """
 209
 210        self.searchResultsFile = "search-results.md"
 211        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 212
 213        See also: `SearchInstruments()`.
 214        """
 215
 216        self.pricesFile = "prices.md"
 217        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 218
 219        See also: `GetListOfPrices()`.
 220        """
 221
 222        self.infoFile = "info.md"
 223        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 224
 225        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 226        """
 227
 228        self.bondsXLSXFile = "ext-bonds.xlsx"
 229        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 230        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 231
 232        See also: `ExtendBondsData()`.
 233        """
 234
 235        self.calendarFile = "calendar.md"
 236        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 237        
 238        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 239
 240        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 241        """
 242
 243        self.overviewFile = "overview.md"
 244        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 245
 246        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 247        """
 248
 249        self.overviewDigestFile = "overview-digest.md"
 250        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 251
 252        See also: `Overview()` with parameter `details="digest"`.
 253        """
 254
 255        self.overviewPositionsFile = "overview-positions.md"
 256        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 257
 258        See also: `Overview()` with parameter `details="positions"`.
 259        """
 260
 261        self.overviewOrdersFile = "overview-orders.md"
 262        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 263
 264        See also: `Overview()` with parameter `details="orders"`.
 265        """
 266
 267        self.overviewAnalyticsFile = "overview-analytics.md"
 268        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 269
 270        See also: `Overview()` with parameter `details="analytics"`.
 271        """
 272
 273        self.overviewBondsCalendarFile = "overview-calendar.md"
 274        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 275
 276        See also: `Overview()` with parameter `details="calendar"`.
 277        """
 278
 279        self.reportFile = "deals.md"
 280        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 281
 282        See also: `Deals()`.
 283        """
 284
 285        self.withdrawalLimitsFile = "limits.md"
 286        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 287
 288        See also: `OverviewLimits()` and `RequestLimits()`.
 289        """
 290
 291        self.userInfoFile = "user-info.md"
 292        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 293
 294        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 295        """
 296
 297        self.userAccountsFile = "accounts.md"
 298        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 299
 300        See also: `OverviewAccounts()`, `RequestAccounts()`.
 301        """
 302
 303        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 304        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 305
 306        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 307
 308        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 309        """
 310
 311        self.iList = None  # init iList for raw instruments data
 312        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 313        
 314        See also: `Listing()`, `DumpInstruments()`.
 315        """
 316
 317        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 318        if useCache:
 319            if os.path.exists(self.iListDumpFile):
 320                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 321                curTime = datetime.now(tzutc())
 322
 323                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 324                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 325
 326                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 327
 328                else:
 329                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 330
 331                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 332                        os.path.abspath(self.iListDumpFile),
 333                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 334                    ))
 335
 336            else:
 337                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 338                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 339
 340        else:
 341            self.iList = self.Listing()  # request new raw instruments data from broker server
 342            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 343
 344        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 345        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 346
 347        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 348        """
 349
 350    @property
 351    def ticker(self) -> str:
 352        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 353
 354        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 355        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 356
 357        See also: `SearchByTicker()`, `SearchInstruments()`.
 358        """
 359        return self._ticker
 360
 361    @ticker.setter
 362    def ticker(self, value):
 363        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 364
 365        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 366        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 367
 368        See also: `SearchByTicker()`, `SearchInstruments()`.
 369        """
 370        self._ticker = str(value).upper()  # Tickers may be upper case only
 371
 372    @property
 373    def figi(self) -> str:
 374        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 375
 376        See also: `SearchByFIGI()`, `SearchInstruments()`.
 377        """
 378        return self._figi
 379
 380    @figi.setter
 381    def figi(self, value):
 382        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 383
 384        See also: `SearchByFIGI()`, `SearchInstruments()`.
 385        """
 386        self._figi = str(value).upper()  # FIGI may be upper case only
 387
 388    def _ParseJSON(self, rawData="{}") -> dict:
 389        """
 390        Parse JSON from response string.
 391
 392        :param rawData: this is a string with JSON-formatted text.
 393        :return: JSON (dictionary), parsed from server response string.
 394        """
 395        responseJSON = json.loads(rawData) if rawData else {}
 396
 397        if self.moreDebug:
 398            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 399
 400        return responseJSON
 401
 402    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 403        """
 404        Send GET or POST request to broker server and receive JSON object.
 405
 406        self.header: must be defining with dictionary of headers.
 407        self.body: if define then used as request body. None by default.
 408        self.timeout: global request timeout, 15 seconds by default.
 409        :param url: url with REST request.
 410        :param reqType: send "GET" or "POST" request. "GET" by default.
 411        :param retry: how many times retry after first request if an 5xx server errors occurred.
 412        :param pause: sleep time in seconds between retries.
 413        :return: response JSON (dictionary) from broker.
 414        """
 415        if reqType.upper() not in ("GET", "POST"):
 416            uLogger.error("You can define request type: `GET` or `POST`!")
 417            raise Exception("Incorrect value")
 418
 419        if self.moreDebug:
 420            uLogger.debug("Request parameters:")
 421            uLogger.debug("    - REST API URL: {}".format(url))
 422            uLogger.debug("    - request type: {}".format(reqType))
 423            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 424            uLogger.debug("    - body:\n{}".format(self.body))
 425
 426        # fast hack to avoid all operations with some tickers/FIGI
 427        responseJSON = {}
 428        oK = True
 429        for item in self.exclude:
 430            if item in url:
 431                if self.moreDebug:
 432                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 433
 434                oK = False
 435                break
 436
 437        if oK:
 438            counter = 0
 439            response = None
 440            errMsg = ""
 441
 442            while not response and counter <= retry:
 443                if reqType == "GET":
 444                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 445
 446                if reqType == "POST":
 447                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 448
 449                if self.moreDebug:
 450                    uLogger.debug("Response:")
 451                    uLogger.debug("    - status code: {}".format(response.status_code))
 452                    uLogger.debug("    - reason: {}".format(response.reason))
 453                    uLogger.debug("    - body length: {}".format(len(response.text)))
 454                    uLogger.debug("    - headers:\n{}".format(response.headers))
 455
 456                # Server returns some headers:
 457                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 458                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 459                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 460                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 461                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 462                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 463                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 464                    sleep(rateLimitWait)
 465
 466                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 467                if 400 <= response.status_code < 500:
 468                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 469                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 470
 471                    if "code" in response.text and "message" in response.text:
 472                        msgDict = self._ParseJSON(rawData=response.text)
 473                        uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 474
 475                    counter = retry + 1  # do not retry for 4xx errors
 476
 477                if 500 <= response.status_code < 600:
 478                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 479                    uLogger.debug("    - not oK, {}".format(errMsg))
 480
 481                    if "code" in response.text and "message" in response.text:
 482                        errMsgDict = self._ParseJSON(rawData=response.text)
 483                        uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 484
 485                    counter += 1
 486
 487                    if counter <= retry:
 488                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 489                        sleep(pause)
 490
 491            responseJSON = self._ParseJSON(rawData=response.text)
 492
 493            if errMsg:
 494                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 495                uLogger.error("    - not oK, {}".format(errMsg))
 496
 497        return responseJSON
 498
 499    def _IUpdater(self, iType: str) -> tuple:
 500        """
 501        Request instrument by type from server. See available API methods for instruments:
 502        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 503        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 504        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 505        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 506        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 507
 508        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 509        :return: tuple with iType name and list of available instruments of current type for defined user token.
 510        """
 511        result = []
 512
 513        if iType in TKS_INSTRUMENTS:
 514            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 515
 516            # all instruments have the same body in API v2 requests:
 517            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 518            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 519            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 520
 521        return iType, result
 522
 523    def _IWrapper(self, kwargs):
 524        """
 525        Wrapper runs instrument's update method `_IUpdater()`.
 526        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 527        """
 528        return self._IUpdater(**kwargs)
 529
 530    def Listing(self) -> dict:
 531        """
 532        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 533
 534        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 535        """
 536        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 537        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 538
 539        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 540        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 541        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 542
 543        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 544        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 545        poolUpdater.close()
 546
 547        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 548        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 549        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 550
 551        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 552        for iType in iList.keys():
 553            for ticker in iList[iType]:
 554                iList[iType][ticker]["type"] = iType
 555
 556                if "minPriceIncrement" in iList[iType][ticker].keys():
 557                    iList[iType][ticker]["step"] = NanoToFloat(
 558                        iList[iType][ticker]["minPriceIncrement"]["units"],
 559                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 560                    )
 561
 562                else:
 563                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 564
 565        return iList
 566
 567    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 568        """
 569        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 570
 571        See also: `DumpInstruments()`, `Listing()`.
 572
 573        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 574                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 575        """
 576        if self.iListDumpFile is None or not self.iListDumpFile:
 577            uLogger.error("Output name of dump file must be defined!")
 578            raise Exception("Filename required")
 579
 580        if not self.iList or forceUpdate:
 581            self.iList = self.Listing()
 582
 583        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 584
 585        # Save as XLSX with separated sheets for every type of instruments:
 586        with pd.ExcelWriter(
 587                path=xlsxDumpFile,
 588                date_format=TKS_DATE_FORMAT,
 589                datetime_format=TKS_DATE_TIME_FORMAT,
 590                mode="w",
 591        ) as writer:
 592            for iType in TKS_INSTRUMENTS:
 593                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 594                df = df[sorted(df)]  # sorted by column names
 595                df = df.applymap(
 596                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 597                    na_action="ignore",
 598                )  # converting numbers from nano-type to float in every cell
 599                df.to_excel(
 600                    writer,
 601                    sheet_name=iType,
 602                    encoding="UTF-8",
 603                    freeze_panes=(1, 1),
 604                )  # saving as XLSX-file with freeze first row and column as headers
 605
 606        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 607
 608    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 609        """
 610        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 611        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 612
 613        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 614
 615        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 616                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 617        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 618        """
 619        if self.iListDumpFile is None or not self.iListDumpFile:
 620            uLogger.error("Output name of dump file must be defined!")
 621            raise Exception("Filename required")
 622
 623        if not self.iList or forceUpdate:
 624            self.iList = self.Listing()
 625
 626        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 627        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 628            fH.write(jsonDump)
 629
 630        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 631
 632        return jsonDump
 633
 634    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 635        """
 636        Show information about one instrument defined by json data and prints it in Markdown format.
 637
 638        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 639
 640        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 641        :param show: if `True` then also printing information about instrument and its current price.
 642        :return: multilines text in Markdown format with information about one instrument.
 643        """
 644        splitLine = "|                                                             |                                                        |\n"
 645        infoText = ""
 646
 647        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 648            info = [
 649                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 650                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 651                "| Parameters                                                  | Values                                                 |\n",
 652                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 653                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 654                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 655            ]
 656
 657            if "sector" in iJSON.keys() and iJSON["sector"]:
 658                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 659
 660            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 661                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 662
 663            info.extend([
 664                splitLine,
 665                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 666                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 667            ])
 668
 669            if "isin" in iJSON.keys() and iJSON["isin"]:
 670                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 671
 672            if "classCode" in iJSON.keys():
 673                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 674
 675            info.extend([
 676                splitLine,
 677                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 678                splitLine,
 679                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 680                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 681                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 682            ])
 683
 684            if iJSON["figi"]:
 685                self._figi = iJSON["figi"]
 686                iJSON = iJSON | self.RequestTradingStatus()
 687
 688                info.extend([
 689                    splitLine,
 690                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 691                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 692                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 693                ])
 694
 695            info.append(splitLine)
 696
 697            if "type" in iJSON.keys() and iJSON["type"]:
 698                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 699
 700                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 701                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 702
 703            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 704                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 705
 706            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 707                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 708
 709            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 710                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 711
 712            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 713                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 714
 715            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 716                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 717
 718            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 719                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 720
 721            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 722                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 723
 724            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 725                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 726
 727            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 728                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 729
 730            if "currency" in iJSON.keys():
 731                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 732
 733            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 734                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 735
 736            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 737                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 738
 739            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 740                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 741
 742            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 743                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 744
 745            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 746                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 747
 748            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 749                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 750
 751            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 752                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 753
 754            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 755                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 756
 757            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 758                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 759
 760            iExt = None
 761            if iJSON["type"] == "Bonds":
 762                info.extend([
 763                    splitLine,
 764                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 765                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 766                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 767                        iJSON["nominal"]["currency"],
 768                    )),
 769                ])
 770
 771                if "floatingCouponFlag" in iJSON.keys():
 772                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 773
 774                if "amortizationFlag" in iJSON.keys():
 775                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 776
 777                info.append(splitLine)
 778
 779                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 780                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 781
 782                if iJSON["figi"]:
 783                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 784
 785                    info.extend([
 786                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 787                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 788                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 789                    ])
 790
 791                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 792                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 793                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 794                        iJSON["aciValue"]["currency"]
 795                    )))
 796
 797            if "currentPrice" in iJSON.keys():
 798                info.append(splitLine)
 799
 800                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 801                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 802
 803                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 804                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 805                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 806                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 807                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 808
 809                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 810                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 811
 812                info.extend([
 813                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 814                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 815                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 816                    )),
 817                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 818                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 819                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 820                    )),
 821                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 822                        "{:.2f}%{}".format(
 823                            iJSON["currentPrice"]["changes"],
 824                            " ({}{:.2f} {})".format(
 825                                "+" if bondChangesDelta > 0 else "",
 826                                bondChangesDelta,
 827                                aciCurrency
 828                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 829                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 830                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 831                                currency
 832                            ),
 833                        )
 834                    ),
 835                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 836                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 837                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 838                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 839                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 840                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 841                    )),
 842                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 843                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 844                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 845                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 846                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 847                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 848                    )),
 849                ])
 850
 851            if "lot" in iJSON.keys():
 852                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 853
 854            if "step" in iJSON.keys() and iJSON["step"] != 0:
 855                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 856
 857            # Add bond payment calendar:
 858            if iJSON["type"] == "Bonds":
 859                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 860                info.extend(["\n", strCalendar])
 861
 862            infoText += "".join(info)
 863
 864            if show:
 865                uLogger.info("{}".format(infoText))
 866
 867            else:
 868                uLogger.debug("{}".format(infoText))
 869
 870            if self.infoFile is not None:
 871                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 872                    fH.write(infoText)
 873
 874                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 875
 876        return infoText
 877
 878    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 879        """
 880        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 881
 882        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 883        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 884        :return: JSON formatted data with information about instrument.
 885        """
 886        tickerJSON = {}
 887        if self.moreDebug:
 888            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 889
 890        if not self._ticker:
 891            uLogger.warning("self._ticker variable is not be empty!")
 892
 893        else:
 894            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 895                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 896                raise Exception("Instrument not allowed")
 897
 898            if not self.iList:
 899                self.iList = self.Listing()
 900
 901            if self._ticker in self.iList["Shares"].keys():
 902                tickerJSON = self.iList["Shares"][self._ticker]
 903                if self.moreDebug:
 904                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 905
 906            elif self._ticker in self.iList["Currencies"].keys():
 907                tickerJSON = self.iList["Currencies"][self._ticker]
 908                if self.moreDebug:
 909                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 910
 911            elif self._ticker in self.iList["Bonds"].keys():
 912                tickerJSON = self.iList["Bonds"][self._ticker]
 913                if self.moreDebug:
 914                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 915
 916            elif self._ticker in self.iList["Etfs"].keys():
 917                tickerJSON = self.iList["Etfs"][self._ticker]
 918                if self.moreDebug:
 919                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 920
 921            elif self._ticker in self.iList["Futures"].keys():
 922                tickerJSON = self.iList["Futures"][self._ticker]
 923                if self.moreDebug:
 924                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 925
 926        if tickerJSON:
 927            self._figi = tickerJSON["figi"]
 928
 929            if requestPrice:
 930                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 931
 932                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 933                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 934
 935                else:
 936                    tickerJSON["currentPrice"]["changes"] = 0
 937
 938            if show:
 939                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 940
 941        else:
 942            if show:
 943                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 944
 945        return tickerJSON
 946
 947    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 948        """
 949        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 950
 951        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 952        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 953        :return: JSON formatted data with information about instrument.
 954        """
 955        figiJSON = {}
 956        if self.moreDebug:
 957            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 958
 959        if not self._figi:
 960            uLogger.warning("self._figi variable is not be empty!")
 961
 962        else:
 963            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 964                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 965                raise Exception("Instrument not allowed")
 966
 967            if not self.iList:
 968                self.iList = self.Listing()
 969
 970            for item in self.iList["Shares"].keys():
 971                if self._figi == self.iList["Shares"][item]["figi"]:
 972                    figiJSON = self.iList["Shares"][item]
 973
 974                    if self.moreDebug:
 975                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
 976
 977                    break
 978
 979            if not figiJSON:
 980                for item in self.iList["Currencies"].keys():
 981                    if self._figi == self.iList["Currencies"][item]["figi"]:
 982                        figiJSON = self.iList["Currencies"][item]
 983
 984                        if self.moreDebug:
 985                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
 986
 987                        break
 988
 989            if not figiJSON:
 990                for item in self.iList["Bonds"].keys():
 991                    if self._figi == self.iList["Bonds"][item]["figi"]:
 992                        figiJSON = self.iList["Bonds"][item]
 993
 994                        if self.moreDebug:
 995                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
 996
 997                        break
 998
 999            if not figiJSON:
1000                for item in self.iList["Etfs"].keys():
1001                    if self._figi == self.iList["Etfs"][item]["figi"]:
1002                        figiJSON = self.iList["Etfs"][item]
1003
1004                        if self.moreDebug:
1005                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1006
1007                        break
1008
1009            if not figiJSON:
1010                for item in self.iList["Futures"].keys():
1011                    if self._figi == self.iList["Futures"][item]["figi"]:
1012                        figiJSON = self.iList["Futures"][item]
1013
1014                        if self.moreDebug:
1015                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1016
1017                        break
1018
1019        if figiJSON:
1020            self._figi = figiJSON["figi"]
1021            self._ticker = figiJSON["ticker"]
1022
1023            if requestPrice:
1024                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1025
1026                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1027                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1028
1029                else:
1030                    figiJSON["currentPrice"]["changes"] = 0
1031
1032            if show:
1033                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1034
1035        else:
1036            if show:
1037                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1038
1039        return figiJSON
1040
1041    def GetCurrentPrices(self, show: bool = True) -> dict:
1042        """
1043        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1044        `{"buy": [{"price": 1243.8, "quantity": 193},
1045                  {"price": 1244.0, "quantity": 168},
1046                  {"price": 1244.8, "quantity": 5},
1047                  {"price": 1245.0, "quantity": 61},
1048                  {"price": 1245.4, "quantity": 60}],
1049          "sell": [{"price": 1243.6, "quantity": 8},
1050                   {"price": 1242.6, "quantity": 10},
1051                   {"price": 1242.4, "quantity": 18},
1052                   {"price": 1242.2, "quantity": 50},
1053                   {"price": 1242.0, "quantity": 113}],
1054          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1055        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1056        - sell: list of dicts with Buyers prices,
1057            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1058            - quantity: volume value by current price in lots,
1059        - limitUp: current trade session limit price, maximum,
1060        - limitDown: current trade session limit price, minimum,
1061        - lastPrice: last deal price of the instrument,
1062        - closePrice: previous trade session close price of the instrument.
1063
1064        See also: `SearchByTicker()` and `SearchByFIGI()`.
1065        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1066        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1067
1068        :param show: if `True` then print DOM to log and console.
1069        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1070                 If an error occurred then returns an empty record:
1071                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1072        """
1073        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1074
1075        if self.depth < 1:
1076            uLogger.error("Depth of Market (DOM) must be >=1!")
1077            raise Exception("Incorrect value")
1078
1079        if not (self._ticker or self._figi):
1080            uLogger.error("self._ticker or self._figi variables must be defined!")
1081            raise Exception("Ticker or FIGI required")
1082
1083        if self._ticker and not self._figi:
1084            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1085            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1086
1087        if not self._ticker and self._figi:
1088            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1089            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1090
1091        if not self._figi:
1092            uLogger.error("FIGI is not defined!")
1093            raise Exception("Ticker or FIGI required")
1094
1095        else:
1096            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1097
1098            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1099            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1100            self.body = str({"figi": self._figi, "depth": self.depth})
1101            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1102
1103            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1104                # list of dicts with sellers orders:
1105                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1106
1107                # list of dicts with buyers orders:
1108                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1109
1110                # max price of instrument at this time:
1111                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1112
1113                # min price of instrument at this time:
1114                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1115
1116                # last price of deal with instrument:
1117                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1118
1119                # last close price of instrument:
1120                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1121
1122            else:
1123                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1124                uLogger.debug("Server response: {}".format(pricesResponse))
1125
1126            if show:
1127                if prices["buy"] or prices["sell"]:
1128                    info = [
1129                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1130                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1131                            self._ticker,
1132                            self._figi,
1133                            self.depth,
1134                        ),
1135                        "-" * 60, "\n",
1136                        "             Orders of Buyers | Orders of Sellers\n",
1137                        "-" * 60, "\n",
1138                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1139                        "-" * 60, "\n",
1140                    ]
1141
1142                    if not prices["buy"]:
1143                        info.append("                              | No orders!\n")
1144                        sumBuy = 0
1145
1146                    else:
1147                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1148                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1149                        for item in maxMinSorted:
1150                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1151
1152                    if not prices["sell"]:
1153                        info.append("No orders!                    |\n")
1154                        sumSell = 0
1155
1156                    else:
1157                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1158                        for item in prices["sell"]:
1159                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1160
1161                    info.extend([
1162                        "-" * 60, "\n",
1163                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1164                        "-" * 60, "\n",
1165                    ])
1166
1167                    infoText = "".join(info)
1168
1169                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1170
1171                else:
1172                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1173
1174        return prices
1175
1176    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1177        """
1178        This method get and show information about all available broker instruments for current user account.
1179        If `instrumentsFile` string is not empty then also save information to this file.
1180
1181        :param show: if `True` then print results to console, if `False` — print only to file.
1182        :return: multi-lines string with all available broker instruments
1183        """
1184        if not self.iList:
1185            self.iList = self.Listing()
1186
1187        info = [
1188            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1189            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1190        ]
1191
1192        # add instruments count by type:
1193        for iType in self.iList.keys():
1194            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1195
1196        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1197        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1198
1199        # generating info tables with all instruments by type:
1200        for iType in self.iList.keys():
1201            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1202
1203            for instrument in self.iList[iType].keys():
1204                iName = self.iList[iType][instrument]["name"]  # instrument's name
1205                if len(iName) > 57:
1206                    iName = "{}...".format(iName[:54])  # right trim for a long string
1207
1208                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1209                    self.iList[iType][instrument]["ticker"],
1210                    iName,
1211                    self.iList[iType][instrument]["figi"],
1212                    self.iList[iType][instrument]["currency"],
1213                    self.iList[iType][instrument]["lot"],
1214                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1215                ))
1216
1217        infoText = "".join(info)
1218
1219        if show:
1220            uLogger.info(infoText)
1221
1222        if self.instrumentsFile:
1223            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1224                fH.write(infoText)
1225
1226            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1227
1228        return infoText
1229
1230    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1231        """
1232        This method search and show information about instruments by part of its ticker, FIGI or name.
1233        If `searchResultsFile` string is not empty then also save information to this file.
1234
1235        :param pattern: string with part of ticker, FIGI or instrument's name.
1236        :param show: if `True` then print results to console, if `False` — return list of result only.
1237        :return: list of dictionaries with all found instruments.
1238        """
1239        if not self.iList:
1240            self.iList = self.Listing()
1241
1242        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1243        compiledPattern = re.compile(pattern, re.IGNORECASE)
1244
1245        for iType in self.iList:
1246            for instrument in self.iList[iType].values():
1247                searchResult = compiledPattern.search(" ".join(
1248                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1249                ))
1250
1251                if searchResult:
1252                    searchResults[iType][instrument["ticker"]] = instrument
1253
1254        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1255        info = [
1256            "# Search results\n\n",
1257            "* **Search pattern:** [{}]\n".format(pattern),
1258            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1259            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1260        ]
1261        infoShort = info[:]
1262
1263        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1264        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1265        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1266
1267        if resultsLen == 0:
1268            info.append("\nNo results\n")
1269            infoShort.append("\nNo results\n")
1270            uLogger.warning("No results. Try changing your search pattern.")
1271
1272        else:
1273            for iType in searchResults:
1274                iTypeValuesCount = len(searchResults[iType].values())
1275                if iTypeValuesCount > 0:
1276                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1277                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1278
1279                    for instrument in searchResults[iType].values():
1280                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1281                            instrument["type"],
1282                            instrument["ticker"],
1283                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1284                            instrument["figi"],
1285                        ))
1286
1287                    if iTypeValuesCount <= 5:
1288                        infoShort.extend(info[-iTypeValuesCount:])
1289
1290                    else:
1291                        infoShort.extend(info[-5:])
1292                        infoShort.append(skippedLine)
1293
1294        infoText = "".join(info)
1295        infoTextShort = "".join(infoShort)
1296
1297        if show:
1298            uLogger.info(infoTextShort)
1299            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1300
1301        if self.searchResultsFile:
1302            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1303                fH.write(infoText)
1304
1305            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1306
1307        return searchResults
1308
1309    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1310        """
1311        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1312
1313        :param instruments: list of strings with tickers or FIGIs.
1314        :return: list with unique instrument FIGIs only.
1315        """
1316        requestedInstruments = []
1317        for iName in instruments:
1318            if iName not in self.aliases.keys():
1319                if iName not in requestedInstruments:
1320                    requestedInstruments.append(iName)
1321
1322            else:
1323                if iName not in requestedInstruments:
1324                    if self.aliases[iName] not in requestedInstruments:
1325                        requestedInstruments.append(self.aliases[iName])
1326
1327        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1328
1329        onlyUniqueFIGIs = []
1330        for iName in requestedInstruments:
1331            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1332                continue
1333
1334            self._ticker = iName
1335            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1336
1337            if not iData:
1338                self._ticker = ""
1339                self._figi = iName
1340
1341                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1342
1343                if not iData:
1344                    self._figi = ""
1345                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1346
1347            if iData and iData["figi"] not in onlyUniqueFIGIs:
1348                onlyUniqueFIGIs.append(iData["figi"])
1349
1350        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1351
1352        return onlyUniqueFIGIs
1353
1354    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1355        """
1356        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1357
1358        See limits: https://tinkoff.github.io/investAPI/limits/
1359
1360        If `pricesFile` string is not empty then also save information to this file.
1361
1362        :param instruments: list of strings with tickers or FIGIs.
1363        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1364        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1365                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1366        """
1367        if instruments is None or not instruments:
1368            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1369            raise Exception("Ticker or FIGI required")
1370
1371        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1372
1373        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1374
1375        iList = []  # trying to get info and current prices about all unique instruments:
1376        for self._figi in onlyUniqueFIGIs:
1377            iData = self.SearchByFIGI(requestPrice=True)
1378            iList.append(iData)
1379
1380        self.ShowListOfPrices(iList, show)
1381
1382        return iList
1383
1384    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1385        """
1386        Show table contains current prices of given instruments.
1387
1388        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1389                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1390        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1391        :return: multilines text in Markdown format as a table contains current prices.
1392        """
1393        infoText = ""
1394
1395        if show or self.pricesFile:
1396            info = [
1397                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1398                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1399                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1400            ]
1401
1402            for item in iList:
1403                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1404                    item["ticker"],
1405                    item["figi"],
1406                    item["type"],
1407                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1408                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1409                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1410                    "{} / {}".format(
1411                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1412                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1413                    ),
1414                    "{} / {}".format(
1415                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1416                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1417                    ),
1418                    item["currency"],
1419                ))
1420
1421            infoText = "".join(info)
1422
1423            if show:
1424                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1425
1426            if self.pricesFile:
1427                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1428                    fH.write(infoText)
1429
1430                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1431
1432        return infoText
1433
1434    def RequestTradingStatus(self) -> dict:
1435        """
1436        Requesting trading status for the instrument defined by `figi` variable.
1437
1438        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1439
1440        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1441
1442        :return: dictionary with trading status attributes. Response example:
1443                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1444                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1445        """
1446        if self._figi is None or not self._figi:
1447            uLogger.error("Variable `figi` must be defined for using this method!")
1448            raise Exception("FIGI required")
1449
1450        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1451
1452        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1453        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1454        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1455
1456        if self.moreDebug:
1457            uLogger.debug("Records about current trading status successfully received")
1458
1459        return tradingStatus
1460
1461    def RequestPortfolio(self) -> dict:
1462        """
1463        Requesting actual user's portfolio for current `accountId`.
1464
1465        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1466
1467        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1468
1469        :return: dictionary with user's portfolio.
1470        """
1471        if self.accountId is None or not self.accountId:
1472            uLogger.error("Variable `accountId` must be defined for using this method!")
1473            raise Exception("Account ID required")
1474
1475        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1476
1477        self.body = str({"accountId": self.accountId})
1478        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1479        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1480
1481        if self.moreDebug:
1482            uLogger.debug("Records about user's portfolio successfully received")
1483
1484        return rawPortfolio
1485
1486    def RequestPositions(self) -> dict:
1487        """
1488        Requesting open positions by currencies and instruments for current `accountId`.
1489
1490        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1491
1492        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1493
1494        :return: dictionary with open positions by instruments.
1495        """
1496        if self.accountId is None or not self.accountId:
1497            uLogger.error("Variable `accountId` must be defined for using this method!")
1498            raise Exception("Account ID required")
1499
1500        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1501
1502        self.body = str({"accountId": self.accountId})
1503        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1504        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1505
1506        if self.moreDebug:
1507            uLogger.debug("Records about current open positions successfully received")
1508
1509        return rawPositions
1510
1511    def RequestPendingOrders(self) -> list:
1512        """
1513        Requesting current actual pending limit orders for current `accountId`.
1514
1515        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1516
1517        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1518
1519        :return: list of dictionaries with pending limit orders.
1520        """
1521        if self.accountId is None or not self.accountId:
1522            uLogger.error("Variable `accountId` must be defined for using this method!")
1523            raise Exception("Account ID required")
1524
1525        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1526
1527        self.body = str({"accountId": self.accountId})
1528        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1529        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1530
1531        uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1532
1533        return rawOrders
1534
1535    def RequestStopOrders(self) -> list:
1536        """
1537        Requesting current actual stop orders for current `accountId`.
1538
1539        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1540
1541        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1542
1543        :return: list of dictionaries with stop orders.
1544        """
1545        if self.accountId is None or not self.accountId:
1546            uLogger.error("Variable `accountId` must be defined for using this method!")
1547            raise Exception("Account ID required")
1548
1549        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1550
1551        self.body = str({"accountId": self.accountId})
1552        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1553        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1554
1555        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1556
1557        return rawStopOrders
1558
1559    def Overview(self, show: bool = False, details: str = "full") -> dict:
1560        """
1561        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1562        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1563        and `overviewBondsCalendarFile` are defined then also save information to file.
1564
1565        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1566        many requests about the state of the portfolio, and then, based on the received data, a large number
1567        of calculation and statistics are collected.
1568
1569        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1570        :param details: how detailed should the information be?
1571        - `full` — shows full available information about portfolio status (by default),
1572        - `positions` — shows only open positions,
1573        - `orders` — shows only sections of open limits and stop orders.
1574        - `digest` — show a short digest of the portfolio status,
1575        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1576        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1577        :return: dictionary with client's raw portfolio and some statistics.
1578        """
1579        if self.accountId is None or not self.accountId:
1580            uLogger.error("Variable `accountId` must be defined for using this method!")
1581            raise Exception("Account ID required")
1582
1583        view = {
1584            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1585                "headers": {},  # list of dictionaries, response headers without "positions" section
1586                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1587                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1588                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1589                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1590                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1591                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1592                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1593                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1594                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1595            },
1596            "stat": {  # --- some statistics calculated using "raw" sections:
1597                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1598                "availableRUB": 0.,  # available rubles (without other currencies)
1599                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1600                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1601                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1602                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1603                "sharesCostRUB": 0.,  # costs of all shares in RUB
1604                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1605                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1606                "futuresCostRUB": 0.,  # costs of all futures in RUB
1607                "Currencies": [],  # list of dictionaries of all currencies statistics
1608                "Shares": [],  # list of dictionaries of all shares statistics
1609                "Bonds": [],  # list of dictionaries of all bonds statistics
1610                "Etfs": [],  # list of dictionaries of all etfs statistics
1611                "Futures": [],  # list of dictionaries of all futures statistics
1612                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1613                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1614                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1615                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1616                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1617            },
1618            "analytics": {  # --- some analytics of portfolio:
1619                "distrByAssets": {},  # portfolio distribution by assets
1620                "distrByCompanies": {},  # portfolio distribution by companies
1621                "distrBySectors": {},  # portfolio distribution by sectors
1622                "distrByCurrencies": {},  # portfolio distribution by currencies
1623                "distrByCountries": {},  # portfolio distribution by countries
1624                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1625            }
1626        }
1627
1628        details = details.lower()
1629        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1630        if details not in availableDetails:
1631            details = "full"
1632            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1633
1634        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1635
1636        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1637        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1638        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1639        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1640
1641        # save response headers without "positions" section:
1642        for key in portfolioResponse.keys():
1643            if key != "positions":
1644                view["raw"]["headers"][key] = portfolioResponse[key]
1645
1646            else:
1647                continue
1648
1649        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1650        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1651        for item in portfolioResponse["positions"]:
1652            if item["instrumentType"] == "currency":
1653                self._figi = item["figi"]
1654                curr = self.SearchByFIGI(requestPrice=False)
1655
1656                # current price of currency in RUB:
1657                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1658                    "name": curr["name"],
1659                    "currentPrice": NanoToFloat(
1660                        item["currentPrice"]["units"],
1661                        item["currentPrice"]["nano"]
1662                    ),
1663                }
1664
1665                view["raw"]["Currencies"].append(item)
1666
1667            elif item["instrumentType"] == "share":
1668                view["raw"]["Shares"].append(item)
1669
1670            elif item["instrumentType"] == "bond":
1671                view["raw"]["Bonds"].append(item)
1672
1673            elif item["instrumentType"] == "etf":
1674                view["raw"]["Etfs"].append(item)
1675
1676            elif item["instrumentType"] == "futures":
1677                view["raw"]["Futures"].append(item)
1678
1679            else:
1680                continue
1681
1682        # how many volume of currencies (by ISO currency name) are blocked:
1683        for item in view["raw"]["positions"]["blocked"]:
1684            blocked = NanoToFloat(item["units"], item["nano"])
1685            if blocked > 0:
1686                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1687
1688        # how many volume of instruments (by FIGI) are blocked:
1689        for item in view["raw"]["positions"]["securities"]:
1690            blocked = int(item["blocked"])
1691            if blocked > 0:
1692                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1693
1694        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1695
1696        if "rub" in allBlocked.keys():
1697            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1698
1699        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1700        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1701        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1702        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1703        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1704        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1705        view["stat"]["portfolioCostRUB"] = sum([
1706            view["stat"]["allCurrenciesCostRUB"],
1707            view["stat"]["sharesCostRUB"],
1708            view["stat"]["bondsCostRUB"],
1709            view["stat"]["etfsCostRUB"],
1710            view["stat"]["futuresCostRUB"],
1711        ])
1712
1713        # --- calculating some portfolio statistics:
1714        byComp = {}  # distribution by companies
1715        bySect = {}  # distribution by sectors
1716        byCurr = {}  # distribution by currencies (include RUB)
1717        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1718        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1719
1720        for item in portfolioResponse["positions"]:
1721            self._figi = item["figi"]
1722            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1723
1724            if instrument:
1725                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1726                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1727
1728                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1729                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1730
1731                else:
1732                    blocked = 0
1733
1734                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1735                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1736                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1737                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1738                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1739                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1740                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1741                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1742                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1743                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1744                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1745                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1746
1747                statData = {
1748                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1749                    "ticker": instrument["ticker"],  # ticker by FIGI
1750                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1751                    "volume": volume,  # available volume of instrument
1752                    "lots": lots,  # volume in lots of instrument
1753                    "direction": direction,  # direction of an instrument's position: short or long
1754                    "blocked": blocked,  # blocked volume of currency or instrument
1755                    "currentPrice": curPrice,  # current instrument's price in basic asset
1756                    "average": average,  # current average position price
1757                    "cost": cost,  # current cost of all volume of instrument in basic asset
1758                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1759                    "costRUB": costRUB,  # cost of instrument in ruble
1760                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1761                    "profit": profit,  # expected profit at current moment
1762                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1763                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1764                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1765                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1766                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1767                    "step": instrument["step"],  # minimum price increment
1768                }
1769
1770                # adding distribution by unique countries:
1771                if statData["country"] not in byCountry.keys():
1772                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1773
1774                else:
1775                    byCountry[statData["country"]]["cost"] += costRUB
1776                    byCountry[statData["country"]]["percent"] += percentCostRUB
1777
1778                if item["instrumentType"] != "currency":
1779                    # adding distribution by unique companies:
1780                    if statData["name"]:
1781                        if statData["name"] not in byComp.keys():
1782                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1783
1784                        else:
1785                            byComp[statData["name"]]["cost"] += costRUB
1786                            byComp[statData["name"]]["percent"] += percentCostRUB
1787
1788                    # adding distribution by unique sectors:
1789                    if statData["sector"] not in bySect.keys():
1790                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1791
1792                    else:
1793                        bySect[statData["sector"]]["cost"] += costRUB
1794                        bySect[statData["sector"]]["percent"] += percentCostRUB
1795
1796                # adding distribution by unique currencies:
1797                if currency not in byCurr.keys():
1798                    byCurr[currency] = {
1799                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1800                        "cost": costRUB,
1801                        "percent": percentCostRUB
1802                    }
1803
1804                else:
1805                    byCurr[currency]["cost"] += costRUB
1806                    byCurr[currency]["percent"] += percentCostRUB
1807
1808                # saving statistics for every instrument:
1809                if item["instrumentType"] == "currency":
1810                    view["stat"]["Currencies"].append(statData)
1811
1812                    # update dict with free funds for trading (total - blocked) by currencies
1813                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1814                    view["stat"]["funds"][currency] = {
1815                        "total": volume,
1816                        "totalCostRUB": costRUB,  # total volume cost in rubles
1817                        "free": volume - blocked,
1818                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1819                    }
1820
1821                elif item["instrumentType"] == "share":
1822                    view["stat"]["Shares"].append(statData)
1823
1824                elif item["instrumentType"] == "bond":
1825                    view["stat"]["Bonds"].append(statData)
1826
1827                elif item["instrumentType"] == "etf":
1828                    view["stat"]["Etfs"].append(statData)
1829
1830                elif item["instrumentType"] == "Futures":
1831                    view["stat"]["Futures"].append(statData)
1832
1833                else:
1834                    continue
1835
1836        # total changes in Russian Ruble:
1837        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1838        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1839        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1840        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1841        view["stat"]["funds"]["rub"] = {
1842            "total": view["stat"]["availableRUB"],
1843            "totalCostRUB": view["stat"]["availableRUB"],
1844            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1845            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1846        }
1847
1848        # --- pending limit orders sector data:
1849        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1850        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1851
1852        for item in view["raw"]["orders"]:
1853            self._figi = item["figi"]
1854
1855            if item["figi"] not in uniquePendingOrdersFIGIs:
1856                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1857
1858                uniquePendingOrdersFIGIs.append(item["figi"])
1859                uniquePendingOrders[item["figi"]] = instrument
1860
1861            else:
1862                instrument = uniquePendingOrders[item["figi"]]
1863
1864            if instrument:
1865                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1866                orderType = TKS_ORDER_TYPES[item["orderType"]]
1867                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1868                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1869
1870                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1871                if item["direction"] == "ORDER_DIRECTION_BUY":
1872                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1873
1874                else:
1875                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1876
1877                # requested price for order execution:
1878                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1879
1880                # necessary changes in percent to reach target from current price:
1881                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1882
1883                view["stat"]["orders"].append({
1884                    "orderID": item["orderId"],  # orderId number parameter of current order
1885                    "figi": item["figi"],  # FIGI identification
1886                    "ticker": instrument["ticker"],  # ticker name by FIGI
1887                    "lotsRequested": item["lotsRequested"],  # requested lots value
1888                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1889                    "currentPrice": lastPrice,  # current instrument's price for defined action
1890                    "targetPrice": target,  # requested price for order execution in base currency
1891                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1892                    "percentChanges": changes,  # changes in percent to target from current price
1893                    "currency": item["currency"],  # instrument's currency name
1894                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1895                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1896                    "status": orderState,  # order status from TKS_ORDER_STATES
1897                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1898                })
1899
1900        # --- stop orders sector data:
1901        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1902        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1903
1904        for item in view["raw"]["stopOrders"]:
1905            self._figi = item["figi"]
1906
1907            if item["figi"] not in uniqueStopOrdersFIGIs:
1908                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1909
1910                uniqueStopOrdersFIGIs.append(item["figi"])
1911                uniqueStopOrders[item["figi"]] = instrument
1912
1913            else:
1914                instrument = uniqueStopOrders[item["figi"]]
1915
1916            if instrument:
1917                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1918                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1919                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1920
1921                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1922                if "expirationTime" in item.keys():
1923                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1924                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1925
1926                else:
1927                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1928                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1929
1930                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1931                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1932                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1933
1934                else:
1935                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1936
1937                # requested price when stop-order executed:
1938                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1939
1940                # price for limit-order, set up when stop-order executed:
1941                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1942
1943                # necessary changes in percent to reach target from current price:
1944                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1945
1946                view["stat"]["stopOrders"].append({
1947                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1948                    "figi": item["figi"],  # FIGI identification
1949                    "ticker": instrument["ticker"],  # ticker name by FIGI
1950                    "lotsRequested": item["lotsRequested"],  # requested lots value
1951                    "currentPrice": lastPrice,  # current instrument's price for defined action
1952                    "targetPrice": target,  # requested price for stop-order execution in base currency
1953                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1954                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1955                    "percentChanges": changes,  # changes in percent to target from current price
1956                    "currency": item["currency"],  # instrument's currency name
1957                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1958                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1959                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1960                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1961                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1962                })
1963
1964        # --- calculating data for analytics section:
1965        # portfolio distribution by assets:
1966        view["analytics"]["distrByAssets"] = {
1967            "Ruble": {
1968                "uniques": 1,
1969                "cost": view["stat"]["availableRUB"],
1970                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1971            },
1972            "Currencies": {
1973                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1974                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1975                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1976            },
1977            "Shares": {
1978                "uniques": len(view["stat"]["Shares"]),
1979                "cost": view["stat"]["sharesCostRUB"],
1980                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1981            },
1982            "Bonds": {
1983                "uniques": len(view["stat"]["Bonds"]),
1984                "cost": view["stat"]["bondsCostRUB"],
1985                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1986            },
1987            "Etfs": {
1988                "uniques": len(view["stat"]["Etfs"]),
1989                "cost": view["stat"]["etfsCostRUB"],
1990                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1991            },
1992            "Futures": {
1993                "uniques": len(view["stat"]["Futures"]),
1994                "cost": view["stat"]["futuresCostRUB"],
1995                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1996            },
1997        }
1998
1999        # portfolio distribution by companies:
2000        view["analytics"]["distrByCompanies"]["All money cash"] = {
2001            "ticker": "",
2002            "cost": view["stat"]["allCurrenciesCostRUB"],
2003            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2004        }
2005        view["analytics"]["distrByCompanies"].update(byComp)
2006
2007        # portfolio distribution by sectors:
2008        view["analytics"]["distrBySectors"]["All money cash"] = {
2009            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2010            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2011        }
2012        view["analytics"]["distrBySectors"].update(bySect)
2013
2014        # portfolio distribution by currencies:
2015        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2016            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2017
2018            if self.moreDebug:
2019                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2020
2021        view["analytics"]["distrByCurrencies"].update(byCurr)
2022        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2023        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2024
2025        # portfolio distribution by countries:
2026        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2027            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2028
2029            if self.moreDebug:
2030                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2031
2032        view["analytics"]["distrByCountries"].update(byCountry)
2033        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2034        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2035
2036        # --- Prepare text statistics overview in human-readable:
2037        if show:
2038            # Whatever the value `details`, header not changes:
2039            info = [
2040                "# Client's portfolio\n\n",
2041                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2042                "* **Account ID:** [{}]\n".format(self.accountId),
2043            ]
2044
2045            if details in ["full", "positions", "digest"]:
2046                info.extend([
2047                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2048                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2049                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2050                        view["stat"]["totalChangesRUB"],
2051                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2052                        view["stat"]["totalChangesPercentRUB"],
2053                    ),
2054                ])
2055
2056            if details in ["full", "positions"]:
2057                info.extend([
2058                    "## Open positions\n\n",
2059                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2060                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2061                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2062                        "{:.2f} ({:.2f}) rub".format(
2063                            view["stat"]["availableRUB"],
2064                            view["stat"]["blockedRUB"],
2065                        )
2066                    )
2067                ])
2068
2069                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2070                    return [
2071                        "|                             |                                 |          |              |              |                     |                              |\n",
2072                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2073                            noTradeStr if noTradeStr else typeStr,
2074                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2075                        ),
2076                    ]
2077
2078                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2079                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2080                        "{} [{}]".format(data["ticker"], data["figi"]),
2081                        "{:.2f} ({:.2f}) {}".format(
2082                            data["volume"],
2083                            data["blocked"],
2084                            data["currency"],
2085                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2086                            data["volume"],
2087                            data["blocked"],
2088                        ),
2089                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2090                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2091                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2092                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2093                        "{}{:.2f} {} ({}{:.2f}%)".format(
2094                            "+" if data["profit"] > 0 else "",
2095                            data["profit"], data["baseCurrencyName"],
2096                            "+" if data["percentProfit"] > 0 else "",
2097                            data["percentProfit"],
2098                        ),
2099                    )
2100
2101                # --- Show currencies section:
2102                if view["stat"]["Currencies"]:
2103                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2104                    for item in view["stat"]["Currencies"]:
2105                        info.append(_InfoStr(item, showCurrencyName=True))
2106
2107                else:
2108                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2109
2110                # --- Show shares section:
2111                if view["stat"]["Shares"]:
2112                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2113
2114                    for item in view["stat"]["Shares"]:
2115                        info.append(_InfoStr(item))
2116
2117                else:
2118                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2119
2120                # --- Show bonds section:
2121                if view["stat"]["Bonds"]:
2122                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2123
2124                    for item in view["stat"]["Bonds"]:
2125                        info.append(_InfoStr(item))
2126
2127                else:
2128                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2129
2130                # --- Show etfs section:
2131                if view["stat"]["Etfs"]:
2132                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2133
2134                    for item in view["stat"]["Etfs"]:
2135                        info.append(_InfoStr(item))
2136
2137                else:
2138                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2139
2140                # --- Show futures section:
2141                if view["stat"]["Futures"]:
2142                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2143
2144                    for item in view["stat"]["Futures"]:
2145                        info.append(_InfoStr(item))
2146
2147                else:
2148                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2149
2150            if details in ["full", "orders"]:
2151                # --- Show pending limit orders section:
2152                if view["stat"]["orders"]:
2153                    info.extend([
2154                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2155                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2156                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2157                    ])
2158
2159                    for item in view["stat"]["orders"]:
2160                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2161                            "{} [{}]".format(item["ticker"], item["figi"]),
2162                            item["orderID"],
2163                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2164                            "{} {} ({}{:.2f}%)".format(
2165                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2166                                item["baseCurrencyName"],
2167                                "+" if item["percentChanges"] > 0 else "",
2168                                float(item["percentChanges"]),
2169                            ),
2170                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2171                            item["action"],
2172                            item["type"],
2173                            item["date"],
2174                        ))
2175
2176                else:
2177                    info.append("\n## Total pending limit-orders: 0\n")
2178
2179                # --- Show stop orders section:
2180                if view["stat"]["stopOrders"]:
2181                    info.extend([
2182                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2183                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2184                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2185                    ])
2186
2187                    for item in view["stat"]["stopOrders"]:
2188                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2189                            "{} [{}]".format(item["ticker"], item["figi"]),
2190                            item["orderID"],
2191                            item["lotsRequested"],
2192                            "{} {} ({}{:.2f}%)".format(
2193                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2194                                item["baseCurrencyName"],
2195                                "+" if item["percentChanges"] > 0 else "",
2196                                float(item["percentChanges"]),
2197                            ),
2198                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2199                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2200                            item["action"],
2201                            item["type"],
2202                            item["expType"],
2203                            item["createDate"],
2204                            item["expDate"],
2205                        ))
2206
2207                else:
2208                    info.append("\n## Total stop-orders: 0\n")
2209
2210            if details in ["full", "analytics"]:
2211                # -- Show analytics section:
2212                if view["stat"]["portfolioCostRUB"] > 0:
2213                    info.extend([
2214                        "\n# Analytics\n"
2215                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2216                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2217                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2218                            view["stat"]["totalChangesRUB"],
2219                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2220                            view["stat"]["totalChangesPercentRUB"],
2221                        ),
2222                        "\n## Portfolio distribution by assets\n"
2223                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2224                        "|------------------------------------|---------|---------|--------------------|\n",
2225                    ])
2226
2227                    for key in view["analytics"]["distrByAssets"].keys():
2228                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2229                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2230                                key,
2231                                view["analytics"]["distrByAssets"][key]["uniques"],
2232                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2233                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2234                            ))
2235
2236                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2237
2238                    info.extend([
2239                        "\n## Portfolio distribution by companies\n"
2240                        "\n| Company                                      | Percent | Current cost       |\n",
2241                        aSepLine,
2242                    ])
2243
2244                    for company in view["analytics"]["distrByCompanies"].keys():
2245                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2246                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2247                                "{}{}".format(
2248                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2249                                    company,
2250                                ),
2251                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2252                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2253                            ))
2254
2255                    info.extend([
2256                        "\n## Portfolio distribution by sectors\n"
2257                        "\n| Sector                                       | Percent | Current cost       |\n",
2258                        aSepLine,
2259                    ])
2260
2261                    for sector in view["analytics"]["distrBySectors"].keys():
2262                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2263                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2264                                sector,
2265                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2266                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2267                            ))
2268
2269                    info.extend([
2270                        "\n## Portfolio distribution by currencies\n"
2271                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2272                        aSepLine,
2273                    ])
2274
2275                    for curr in view["analytics"]["distrByCurrencies"].keys():
2276                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2277                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2278                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2279                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2280                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2281                            ))
2282
2283                    info.extend([
2284                        "\n## Portfolio distribution by countries\n"
2285                        "\n| Assets by country                            | Percent | Current cost       |\n",
2286                        aSepLine,
2287                    ])
2288
2289                    for country in view["analytics"]["distrByCountries"].keys():
2290                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2291                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2292                                country,
2293                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2294                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2295                            ))
2296
2297            if details in ["full", "calendar"]:
2298                # -- Show bonds payment calendar section:
2299                if view["stat"]["Bonds"]:
2300                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2301                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2302                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2303
2304                else:
2305                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2306
2307            infoText = "".join(info)
2308
2309            uLogger.info(infoText)
2310
2311            if details == "full" and self.overviewFile:
2312                filename = self.overviewFile
2313
2314            elif details == "digest" and self.overviewDigestFile:
2315                filename = self.overviewDigestFile
2316
2317            elif details == "positions" and self.overviewPositionsFile:
2318                filename = self.overviewPositionsFile
2319
2320            elif details == "orders" and self.overviewOrdersFile:
2321                filename = self.overviewOrdersFile
2322
2323            elif details == "analytics" and self.overviewAnalyticsFile:
2324                filename = self.overviewAnalyticsFile
2325
2326            elif details == "calendar" and self.overviewBondsCalendarFile:
2327                filename = self.overviewBondsCalendarFile
2328
2329            else:
2330                filename = ""
2331
2332            if filename:
2333                with open(filename, "w", encoding="UTF-8") as fH:
2334                    fH.write(infoText)
2335
2336                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2337
2338        return view
2339
2340    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2341        """
2342        Returns history operations between two given dates for current `accountId`.
2343        If `reportFile` string is not empty then also save human-readable report.
2344        Shows some statistical data of closed positions.
2345
2346        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2347        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2348        :param show: if `True` then also prints all records to the console.
2349        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2350        :return: original list of dictionaries with history of deals records from API ("operations" key):
2351                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2352                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2353        """
2354        if self.accountId is None or not self.accountId:
2355            uLogger.error("Variable `accountId` must be defined for using this method!")
2356            raise Exception("Account ID required")
2357
2358        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2359
2360        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2361
2362        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2363        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2364        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2365        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2366        customStat = {}  # custom statistics in additional to responseJSON
2367
2368        # --- output report in human-readable format:
2369        if show or self.reportFile:
2370            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2371            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2372            nextDay = ""
2373
2374            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2375
2376            if len(ops) > 0:
2377                customStat = {
2378                    "opsCount": 0,  # total operations count
2379                    "buyCount": 0,  # buy operations
2380                    "sellCount": 0,  # sell operations
2381                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2382                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2383                    "payIn": {"rub": 0.},  # Deposit brokerage account
2384                    "payOut": {"rub": 0.},  # Withdrawals
2385                    "divs": {"rub": 0.},  # Dividends income
2386                    "coupons": {"rub": 0.},  # Coupon's income
2387                    "brokerCom": {"rub": 0.},  # Service commissions
2388                    "serviceCom": {"rub": 0.},  # Service commissions
2389                    "marginCom": {"rub": 0.},  # Margin commissions
2390                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2391                }
2392
2393                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2394                for item in ops:
2395                    if item["state"] == "OPERATION_STATE_EXECUTED":
2396                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2397
2398                        # count buy operations:
2399                        if "_BUY" in item["operationType"]:
2400                            customStat["buyCount"] += 1
2401
2402                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2403                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2404
2405                            else:
2406                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2407
2408                        # count sell operations:
2409                        elif "_SELL" in item["operationType"]:
2410                            customStat["sellCount"] += 1
2411
2412                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2413                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2414
2415                            else:
2416                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2417
2418                        # count incoming operations:
2419                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2420                            if item["payment"]["currency"] in customStat["payIn"].keys():
2421                                customStat["payIn"][item["payment"]["currency"]] += payment
2422
2423                            else:
2424                                customStat["payIn"][item["payment"]["currency"]] = payment
2425
2426                        # count withdrawals operations:
2427                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2428                            if item["payment"]["currency"] in customStat["payOut"].keys():
2429                                customStat["payOut"][item["payment"]["currency"]] += payment
2430
2431                            else:
2432                                customStat["payOut"][item["payment"]["currency"]] = payment
2433
2434                        # count dividends income:
2435                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2436                            if item["payment"]["currency"] in customStat["divs"].keys():
2437                                customStat["divs"][item["payment"]["currency"]] += payment
2438
2439                            else:
2440                                customStat["divs"][item["payment"]["currency"]] = payment
2441
2442                        # count coupon's income:
2443                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2444                            if item["payment"]["currency"] in customStat["coupons"].keys():
2445                                customStat["coupons"][item["payment"]["currency"]] += payment
2446
2447                            else:
2448                                customStat["coupons"][item["payment"]["currency"]] = payment
2449
2450                        # count broker commissions:
2451                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2452                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2453                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2454
2455                            else:
2456                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2457
2458                        # count service commissions:
2459                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2460                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2461                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2462
2463                            else:
2464                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2465
2466                        # count margin commissions:
2467                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2468                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2469                                customStat["marginCom"][item["payment"]["currency"]] += payment
2470
2471                            else:
2472                                customStat["marginCom"][item["payment"]["currency"]] = payment
2473
2474                        # count withholding taxes:
2475                        elif "_TAX" in item["operationType"]:
2476                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2477                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2478
2479                            else:
2480                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2481
2482                        else:
2483                            continue
2484
2485                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2486
2487                # --- view "Actions" lines:
2488                info.extend([
2489                    "| Report sections            |                               |                              |                      |                        |\n",
2490                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2491                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2492                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2493                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2494                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2495                    ),
2496                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2497                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2498                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2499                    ),
2500                ])
2501
2502                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2503                for key in opsKeys:
2504                    if key == "rub":
2505                        continue
2506
2507                    info.extend([
2508                        "|                            |                               | {:<28} |                      |                        |\n".format(
2509                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2510                        ),
2511                        "|                            |                               | {:<28} |                      |                        |\n".format(
2512                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2513                        ),
2514                    ])
2515
2516                info.append(splitLine1)
2517
2518                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2519                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2520                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2521                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2522                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2523                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2524                    )
2525
2526                # --- view "Payments" lines:
2527                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2528                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2529
2530                for key in paymentsKeys:
2531                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2532
2533                info.append(splitLine1)
2534
2535                # --- view "Commissions and taxes" lines:
2536                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2537                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2538
2539                for key in comKeys:
2540                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2541
2542                info.append(splitLine1)
2543
2544                info.extend([
2545                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2546                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2547                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2548                ])
2549
2550            else:
2551                info.append("Broker returned no operations during this period\n")
2552
2553            # --- view "Operations" section:
2554            for item in ops:
2555                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2556                    continue
2557
2558                else:
2559                    self._figi = item["figi"] if item["figi"] else ""
2560                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2561                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2562
2563                    # group of deals during one day:
2564                    if nextDay and item["date"].split("T")[0] != nextDay:
2565                        info.append(splitLine2)
2566                        nextDay = ""
2567
2568                    else:
2569                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2570
2571                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2572                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2573                        self._figi if self._figi else "—",
2574                        instrument["ticker"] if instrument else "—",
2575                        instrument["type"] if instrument else "—",
2576                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2577                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2578                        TKS_OPERATION_STATES[item["state"]],
2579                        TKS_OPERATION_TYPES[item["operationType"]],
2580                    ))
2581
2582            infoText = "".join(info)
2583
2584            if show:
2585                if self.moreDebug:
2586                    uLogger.debug("Records about history of a client's operations successfully received")
2587
2588                uLogger.info(infoText)
2589
2590            if self.reportFile:
2591                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2592                    fH.write(infoText)
2593
2594                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2595
2596        return ops, customStat
2597
2598    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2599        """
2600        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2601
2602        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2603        Warning! Broker server used ISO UTC time by default.
2604
2605        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2606        Also, `historyFile` used to update history with `onlyMissing` parameter.
2607
2608        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2609
2610        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2611        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2612        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2613                         `"hour"`, `"day"`. Default: `"hour"`.
2614        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2615                            False by default. Warning! History appends only from last candle to current time
2616                            with always update last candle!
2617        :param csvSep: separator if csv-file is used, `,` by default.
2618        :param show: if `True` then also prints Pandas DataFrame to the console.
2619        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2620                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2621        """
2622        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2623        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2624        history = None  # empty pandas object for history
2625
2626        if interval not in TKS_CANDLE_INTERVALS.keys():
2627            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2628            raise Exception("Incorrect value")
2629
2630        if not (self._ticker or self._figi):
2631            uLogger.error("Ticker or FIGI must be defined!")
2632            raise Exception("Ticker or FIGI required")
2633
2634        if self._ticker and not self._figi:
2635            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2636            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2637
2638        if self._figi and not self._ticker:
2639            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2640            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2641
2642        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2643        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2644        if interval.lower() != "day":
2645            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2646
2647        delta = dtEnd - dtStart  # current UTC time minus last time in file
2648        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2649
2650        # calculate history length in candles:
2651        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2652        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2653            length += 1  # to avoid fraction time
2654
2655        # calculate data blocks count:
2656        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2657
2658        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2659        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2660        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2661        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2662        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2663
2664        tempOld = None  # pandas object for old history, if --only-missing key present
2665        lastTime = None  # datetime object of last old candle in file
2666
2667        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2668            uLogger.debug("--only-missing key present, add only last missing candles...")
2669            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2670
2671            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2672
2673            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2674            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2675            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2676            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2677
2678            # get last datetime object from last string in file or minus 1 delta if file is empty:
2679            if len(tempOld) > 0:
2680                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2681
2682            else:
2683                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2684
2685            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2686
2687        responseJSONs = []  # raw history blocks of data
2688
2689        blockEnd = dtEnd
2690        for item in range(blocks):
2691            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2692            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2693
2694            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2695                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2696            ))
2697
2698            if blockStart == blockEnd:
2699                uLogger.debug("Skipped this zero-length block...")
2700
2701            else:
2702                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2703                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2704                self.body = str({
2705                    "figi": self._figi,
2706                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2707                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2708                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2709                })
2710                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2711
2712                if "code" in responseJSON.keys():
2713                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2714
2715                else:
2716                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2717                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2718
2719                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2720
2721            blockEnd = blockStart
2722
2723        printCount = len(responseJSONs)  # candles to show in console
2724        if responseJSONs:
2725            tempHistory = pd.DataFrame(
2726                data={
2727                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2728                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2729                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2730                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2731                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2732                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2733                    "volume": [int(item["volume"]) for item in responseJSONs],
2734                },
2735                index=range(len(responseJSONs)),
2736                columns=["date", "time", "open", "high", "low", "close", "volume"],
2737            )
2738            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2739            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2740
2741            # append only newest candles to old history if --only-missing key present:
2742            if onlyMissing and tempOld is not None and lastTime is not None:
2743                index = 0  # find start index in tempHistory data:
2744
2745                for i, item in tempHistory.iterrows():
2746                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2747
2748                    if curTime == lastTime:
2749                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2750                        index = i
2751                        printCount = index + 1
2752                        break
2753
2754                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2755
2756            else:
2757                history = tempHistory  # if no `--only-missing` key then load full data from server
2758
2759            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2760
2761        if history is not None and not history.empty:
2762            if show:
2763                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2764                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2765                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2766                ))
2767
2768        else:
2769            uLogger.warning("Received an empty candles history!")
2770
2771        if self.historyFile is not None:
2772            if history is not None and not history.empty:
2773                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2774                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2775
2776            else:
2777                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2778
2779        else:
2780            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2781
2782        return history
2783
2784    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2785        """
2786        Load candles history from csv-file and return Pandas DataFrame object.
2787
2788        See also: `History()` and `ShowHistoryChart()` methods.
2789
2790        :param filePath: path to csv-file to open.
2791        """
2792        loadedHistory = None  # init candles data object
2793
2794        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2795
2796        if os.path.exists(filePath):
2797            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2798
2799            tfStr = self.priceModel.FormattedDelta(
2800                self.priceModel.timeframe,
2801                "{days} days {hours}h {minutes}m {seconds}s",
2802            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2803                self.priceModel.timeframe,
2804                "{hours}h {minutes}m {seconds}s",
2805            )
2806
2807            if loadedHistory is not None and not loadedHistory.empty:
2808                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2809                    len(loadedHistory),
2810                    tfStr,
2811                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2812                )
2813
2814            else:
2815                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2816
2817        else:
2818            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2819
2820        return loadedHistory
2821
2822    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2823        """
2824        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2825
2826        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2827        Default: `index.html` (both for interact and non-interact candlesticks chart).
2828
2829        See also: `History()` and `LoadHistory()` methods.
2830
2831        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2832        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2833                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2834                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2835                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2836        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2837                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2838        """
2839        if isinstance(candles, str):
2840            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2841            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2842
2843        elif isinstance(candles, pd.DataFrame):
2844            self.priceModel.prices = candles  # set candles chain from variable
2845            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2846
2847            if "datetime" not in candles.columns:
2848                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2849
2850        else:
2851            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2852            raise Exception("Incorrect value")
2853
2854        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2855
2856        if interact:
2857            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2858
2859            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2860
2861        else:
2862            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2863
2864            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2865
2866        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2867
2868    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2869        """
2870        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2871        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2872
2873        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2874
2875        :param operation: string "Buy" or "Sell".
2876        :param lots: volume, integer count of lots >= 1.
2877        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2878        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2879        :param expDate: string "Undefined" by default or local date in future,
2880                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2881        :return: JSON with response from broker server.
2882        """
2883        if self.accountId is None or not self.accountId:
2884            uLogger.error("Variable `accountId` must be defined for using this method!")
2885            raise Exception("Account ID required")
2886
2887        if operation is None or not operation or operation not in ("Buy", "Sell"):
2888            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2889            raise Exception("Incorrect value")
2890
2891        if lots is None or lots < 1:
2892            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2893            lots = 1
2894
2895        if tp is None or tp < 0:
2896            tp = 0
2897
2898        if sl is None or sl < 0:
2899            sl = 0
2900
2901        if expDate is None or not expDate:
2902            expDate = "Undefined"
2903
2904        if not (self._ticker or self._figi):
2905            uLogger.error("Ticker or FIGI must be defined!")
2906            raise Exception("Ticker or FIGI required")
2907
2908        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2909        self._ticker = instrument["ticker"]
2910        self._figi = instrument["figi"]
2911
2912        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
2913
2914        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2915        self.body = str({
2916            "figi": self._figi,
2917            "quantity": str(lots),
2918            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2919            "accountId": str(self.accountId),
2920            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2921        })
2922        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2923
2924        if "orderId" in response.keys():
2925            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2926                operation, response["orderId"],
2927                self._ticker, self._figi, lots,
2928                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2929                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2930                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2931            ))
2932
2933            if tp > 0:
2934                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2935
2936            if sl > 0:
2937                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2938
2939        else:
2940            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
2941
2942        return response
2943
2944    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2945        """
2946        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2947        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2948
2949        See also: `Order()` and `Trade()` docstrings.
2950
2951        :param lots: volume, integer count of lots >= 1.
2952        :param tp: float > 0, take profit price of stop-order.
2953        :param sl: float > 0, stop loss price of stop-order.
2954        :param expDate: it's a local date in future.
2955                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2956        :return: JSON with response from broker server.
2957        """
2958        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2959
2960    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2961        """
2962        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2963        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2964
2965        See also: `Order()` and `Trade()` docstrings.
2966
2967        :param lots: volume, integer count of lots >= 1.
2968        :param tp: float > 0, take profit price of stop-order.
2969        :param sl: float > 0, stop loss price of stop-order.
2970        :param expDate: it's a local date in the future.
2971                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2972        :return: JSON with response from broker server.
2973        """
2974        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
2975
2976    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
2977        """
2978        Close position of given instruments.
2979
2980        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
2981        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2982                         This avoids unnecessary downloading data from the server.
2983        """
2984        if instruments is None or not instruments:
2985            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
2986            raise Exception("Ticker or FIGI required")
2987
2988        if isinstance(instruments, str):
2989            instruments = [instruments]
2990
2991        uniqueInstruments = self.GetUniqueFIGIs(instruments)
2992        if uniqueInstruments:
2993            if portfolio is None or not portfolio:
2994                portfolio = self.Overview(show=False)
2995
2996            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
2997            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
2998
2999            for self._figi in uniqueInstruments:
3000                if self._figi not in allOpened:
3001                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3002                    continue
3003
3004                # search open trade info about instrument by ticker:
3005                instrument = {}
3006                for iType in TKS_INSTRUMENTS:
3007                    if instrument:
3008                        break
3009
3010                    for item in portfolio["stat"][iType]:
3011                        if item["figi"] == self._figi:
3012                            instrument = item
3013                            break
3014
3015                if instrument:
3016                    self._ticker = instrument["ticker"]
3017                    self._figi = instrument["figi"]
3018
3019                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3020                        self._ticker,
3021                        self._figi,
3022                        int(instrument["volume"]),
3023                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3024                    ))
3025
3026                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3027
3028                    if tradeLots > 0:
3029                        if instrument["blocked"] > 0:
3030                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3031                                instrument["blocked"],
3032                                self._ticker,
3033                                tradeLots,
3034                            ))
3035
3036                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3037                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3038
3039                    else:
3040                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3041
3042    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3043        """
3044        Close all positions of given instruments with defined type.
3045
3046        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3047        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3048                         This avoids unnecessary downloading data from the server.
3049        """
3050        if iType not in TKS_INSTRUMENTS:
3051            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3052
3053        else:
3054            if portfolio is None or not portfolio:
3055                portfolio = self.Overview(show=False)
3056
3057            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3058            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3059
3060            if tickers and portfolio:
3061                self.CloseTrades(tickers, portfolio)
3062
3063            else:
3064                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3065
3066    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3067        """
3068        Universal method to create market or limit orders with all available parameters for current `accountId`.
3069        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3070
3071        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3072        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3073
3074        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3075        then broker immediately open market order as you can do simple --buy or --sell operations!
3076
3077        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3078        When current price will go up or down to target price value then broker opens a limit order.
3079        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3080
3081        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3082
3083        :param operation: string "Buy" or "Sell".
3084        :param orderType: string "Limit" or "Stop".
3085        :param lots: volume, integer count of lots >= 1.
3086        :param targetPrice: target price > 0. This is open trade price for limit order.
3087        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3088                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3089        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3090                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3091                         Stop loss order always executed by market price.
3092        :param expDate: string "Undefined" by default or local date in future.
3093                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3094                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3095                        A limit order has no expiration date, it lasts until the end of the trading day.
3096        :return: JSON with response from broker server.
3097        """
3098        if self.accountId is None or not self.accountId:
3099            uLogger.error("Variable `accountId` must be defined for using this method!")
3100            raise Exception("Account ID required")
3101
3102        if operation is None or not operation or operation not in ("Buy", "Sell"):
3103            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3104            raise Exception("Incorrect value")
3105
3106        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3107            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3108            raise Exception("Incorrect value")
3109
3110        if lots is None or lots < 1:
3111            uLogger.error("You must define trade volume > 0: integer count of lots!")
3112            raise Exception("Incorrect value")
3113
3114        if targetPrice is None or targetPrice <= 0:
3115            uLogger.error("Target price for limit-order must be greater than 0!")
3116            raise Exception("Incorrect value")
3117
3118        if limitPrice is None or limitPrice <= 0:
3119            limitPrice = targetPrice
3120
3121        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3122            stopType = "Limit"
3123
3124        if expDate is None or not expDate:
3125            expDate = "Undefined"
3126
3127        if not (self._ticker or self._figi):
3128            uLogger.error("Tocker or FIGI must be defined!")
3129            raise Exception("Ticker or FIGI required")
3130
3131        response = {}
3132        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3133        self._ticker = instrument["ticker"]
3134        self._figi = instrument["figi"]
3135
3136        if orderType == "Limit":
3137            uLogger.debug(
3138                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3139                    self._ticker, self._figi,
3140                    operation, lots, targetPrice, instrument["currency"],
3141                ))
3142
3143            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3144            self.body = str({
3145                "figi": self._figi,
3146                "quantity": str(lots),
3147                "price": FloatToNano(targetPrice),
3148                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3149                "accountId": str(self.accountId),
3150                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3151            })
3152            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3153
3154            if "orderId" in response.keys():
3155                uLogger.info(
3156                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3157                        response["orderId"],
3158                        self._ticker, self._figi,
3159                        operation, lots, targetPrice, instrument["currency"],
3160                    ))
3161
3162                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3163                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3164                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3165                            targetPrice, instrument["currency"],
3166                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3167                        ))
3168
3169                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3170                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3171                            targetPrice, instrument["currency"],
3172                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3173                        ))
3174
3175            else:
3176                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3177
3178        if orderType == "Stop":
3179            uLogger.debug(
3180                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3181                    self._ticker, self._figi,
3182                    operation, lots,
3183                    targetPrice, instrument["currency"],
3184                    limitPrice, instrument["currency"],
3185                    stopType, expDate,
3186                ))
3187
3188            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3189            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3190            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3191
3192            body = {
3193                "figi": self._figi,
3194                "quantity": str(lots),
3195                "price": FloatToNano(limitPrice),
3196                "stopPrice": FloatToNano(targetPrice),
3197                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3198                "accountId": str(self.accountId),
3199                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3200                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3201            }
3202
3203            if expDateUTC:
3204                body["expireDate"] = expDateUTC
3205
3206            self.body = str(body)
3207            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3208
3209            if "stopOrderId" in response.keys():
3210                uLogger.info(
3211                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3212                        response["stopOrderId"],
3213                        self._ticker, self._figi,
3214                        operation, lots,
3215                        targetPrice, instrument["currency"],
3216                        limitPrice, instrument["currency"],
3217                        TKS_STOP_ORDER_TYPES[stopOrderType],
3218                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3219                    ))
3220
3221                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3222                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3223                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3224                            targetPrice, instrument["currency"],
3225                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3226                        ))
3227
3228                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3229                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3230                            targetPrice, instrument["currency"],
3231                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3232                        ))
3233
3234            else:
3235                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3236
3237        return response
3238
3239    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3240        """
3241        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3242        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3243        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3244        See also: `Order()` docstring.
3245
3246        :param lots: volume, integer count of lots >= 1.
3247        :param targetPrice: target price > 0. This is open trade price for limit order.
3248        :return: JSON with response from broker server.
3249        """
3250        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3251
3252    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3253        """
3254        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3255        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3256        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3257        target price value then broker opens a limit order. See also: `Order()` docstring.
3258
3259        :param lots: volume, integer count of lots >= 1.
3260        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3261        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3262                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3263        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3264                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3265        :param expDate: string "Undefined" by default or local date in future.
3266                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3267                        This date is converting to UTC format for server.
3268        :return: JSON with response from broker server.
3269        """
3270        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3271
3272    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3273        """
3274        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3275        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3276        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3277        See also: `Order()` docstring.
3278
3279        :param lots: volume, integer count of lots >= 1.
3280        :param targetPrice: target price > 0. This is open trade price for limit order.
3281        :return: JSON with response from broker server.
3282        """
3283        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3284
3285    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3286        """
3287        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3288        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3289        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3290        target price value then broker opens a limit order. See also: `Order()` docstring.
3291
3292        :param lots: volume, integer count of lots >= 1.
3293        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3294        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3295                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3296        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3297                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3298        :param expDate: string "Undefined" by default or local date in future.
3299                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3300                        This date is converting to UTC format for server.
3301        :return: JSON with response from broker server.
3302        """
3303        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3304
3305    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3306        """
3307        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3308
3309        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3310        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3311                             This avoids unnecessary downloading data from the server.
3312        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3313        """
3314        if self.accountId is None or not self.accountId:
3315            uLogger.error("Variable `accountId` must be defined for using this method!")
3316            raise Exception("Account ID required")
3317
3318        if orderIDs:
3319            if allOrdersIDs is None:
3320                rawOrders = self.RequestPendingOrders()
3321                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3322
3323            if allStopOrdersIDs is None:
3324                rawStopOrders = self.RequestStopOrders()
3325                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3326
3327            for orderID in orderIDs:
3328                idInPendingOrders = orderID in allOrdersIDs
3329                idInStopOrders = orderID in allStopOrdersIDs
3330
3331                if not (idInPendingOrders or idInStopOrders):
3332                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3333                    continue
3334
3335                else:
3336                    if idInPendingOrders:
3337                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3338
3339                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3340                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3341                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3342                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3343
3344                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3345                            if self.moreDebug:
3346                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3347
3348                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3349
3350                        else:
3351                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3352
3353                    elif idInStopOrders:
3354                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3355
3356                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3357                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3358                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3359                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3360
3361                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3362                            if self.moreDebug:
3363                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3364
3365                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3366
3367                        else:
3368                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3369
3370                    else:
3371                        continue
3372
3373    def CloseAllOrders(self) -> None:
3374        """
3375        Gets a list of open pending and stop orders and cancel it all.
3376        """
3377        rawOrders = self.RequestPendingOrders()
3378        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3379        lenOrders = len(allOrdersIDs)
3380
3381        rawStopOrders = self.RequestStopOrders()
3382        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3383        lenSOrders = len(allStopOrdersIDs)
3384
3385        if lenOrders > 0 or lenSOrders > 0:
3386            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3387
3388            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3389
3390        else:
3391            uLogger.info("Orders not found, nothing to cancel.")
3392
3393    def CloseAll(self, *args) -> None:
3394        """
3395        Close all available (not blocked) opened trades and orders.
3396
3397        Also, you can select one or more keywords case-insensitive:
3398        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3399
3400        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3401        """
3402        overview = self.Overview(show=False)  # get all open trades info
3403
3404        if len(args) == 0:
3405            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3406            self.CloseAllOrders()  # close all pending and stop orders
3407
3408            for iType in TKS_INSTRUMENTS:
3409                if iType != "Currencies":
3410                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3411
3412        else:
3413            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3414            lowerArgs = [x.lower() for x in args]
3415
3416            if "orders" in lowerArgs:
3417                self.CloseAllOrders()  # close all pending and stop orders
3418
3419            for iType in TKS_INSTRUMENTS:
3420                if iType.lower() in lowerArgs and iType != "Currencies":
3421                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3422
3423    def CloseAllByTicker(self, instrument: str) -> None:
3424        """
3425        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3426
3427        This method searches opened trade and orders of instrument throw all portfolio and then use
3428        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3429
3430        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3431
3432        :param instrument: string with ticker.
3433        """
3434        if instrument is None or not instrument:
3435            uLogger.error("Ticker name must be defined for using this method!")
3436            raise Exception("Ticker required")
3437
3438        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3439
3440        self._ticker = instrument  # try to set instrument as ticker
3441        self._figi = ""
3442
3443        if self.IsInPortfolio(portfolio=overview):
3444            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3445            self.CloseTrades(instruments=[instrument], portfolio=overview)
3446
3447        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3448        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3449
3450        if limitAll and self.IsInLimitOrders(portfolio=overview):
3451            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3452            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3453
3454        if stopAll and self.IsInStopOrders(portfolio=overview):
3455            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3456            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3457
3458    def CloseAllByFIGI(self, instrument: str) -> None:
3459        """
3460        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3461
3462        This method searches opened trade and orders of instrument throw all portfolio and then use
3463        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3464
3465        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3466
3467        :param instrument: string with FIGI id.
3468        """
3469        if instrument is None or not instrument:
3470            uLogger.error("FIGI id must be defined for using this method!")
3471            raise Exception("FIGI required")
3472
3473        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3474
3475        self._ticker = ""
3476        self._figi = instrument  # try to set instrument as FIGI id
3477
3478        if self.IsInPortfolio(portfolio=overview):
3479            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3480            self.CloseTrades(instruments=[instrument], portfolio=overview)
3481
3482        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3483        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3484
3485        if limitAll and self.IsInLimitOrders(portfolio=overview):
3486            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3487            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3488
3489        if stopAll and self.IsInStopOrders(portfolio=overview):
3490            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3491            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3492
3493    @staticmethod
3494    def ParseOrderParameters(operation, **inputParameters):
3495        """
3496        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3497
3498        :param operation: string "Buy" or "Sell".
3499        :param inputParameters: this is dict of strings that looks like this
3500               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3501               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3502               "prices" key: one or more prices to open limit-orders
3503               Counts of values in lots and prices lists must be equals!
3504        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3505        """
3506        # TODO: update order grid work with api v2
3507        pass
3508        # uLogger.debug("Input parameters: {}".format(inputParameters))
3509        #
3510        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3511        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3512        #     raise Exception("Incorrect value")
3513        #
3514        # if "l" in inputParameters.keys():
3515        #     inputParameters["lots"] = inputParameters.pop("l")
3516        #
3517        # if "p" in inputParameters.keys():
3518        #     inputParameters["prices"] = inputParameters.pop("p")
3519        #
3520        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3521        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3522        #     raise Exception("Incorrect value")
3523        #
3524        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3525        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3526        #
3527        # if len(lots) != len(prices):
3528        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3529        #     raise Exception("Incorrect value")
3530        #
3531        # uLogger.debug("Extracted parameters for orders:")
3532        # uLogger.debug("lots = {}".format(lots))
3533        # uLogger.debug("prices = {}".format(prices))
3534        #
3535        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3536        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3537        # uLogger.debug("Order parameters: {}".format(result))
3538        #
3539        # return result
3540
3541    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3542        """
3543        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3544
3545        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3546        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3547        """
3548        result = False
3549        msg = "Instrument not defined!"
3550
3551        if portfolio is None or not portfolio:
3552            portfolio = self.Overview(show=False)
3553
3554        if self._ticker:
3555            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3556            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3557
3558            for iType in TKS_INSTRUMENTS:
3559                for instrument in portfolio["stat"][iType]:
3560                    if instrument["ticker"] == self._ticker:
3561                        result = True
3562                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3563                        break
3564
3565        elif self._figi:
3566            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3567            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3568
3569            for iType in TKS_INSTRUMENTS:
3570                for instrument in portfolio["stat"][iType]:
3571                    if instrument["figi"] == self._figi:
3572                        result = True
3573                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3574                        break
3575
3576        else:
3577            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3578
3579        uLogger.debug(msg)
3580
3581        return result
3582
3583    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3584        """
3585        Returns instrument from the user's portfolio if it presents there.
3586        Instrument must be defined by `ticker` (highly priority) or `figi`.
3587
3588        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3589        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3590        """
3591        result = None
3592        msg = "Instrument not defined!"
3593
3594        if portfolio is None or not portfolio:
3595            portfolio = self.Overview(show=False)
3596
3597        if self._ticker:
3598            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3599            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3600
3601            for iType in TKS_INSTRUMENTS:
3602                for instrument in portfolio["stat"][iType]:
3603                    if instrument["ticker"] == self._ticker:
3604                        result = instrument
3605                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3606                        break
3607
3608        elif self._figi:
3609            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3610            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3611
3612            for iType in TKS_INSTRUMENTS:
3613                for instrument in portfolio["stat"][iType]:
3614                    if instrument["figi"] == self._figi:
3615                        result = instrument
3616                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3617                        break
3618
3619        else:
3620            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3621
3622        uLogger.debug(msg)
3623
3624        return result
3625
3626    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3627        """
3628        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3629
3630        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3631
3632        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3633        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3634        """
3635        result = False
3636        msg = "Instrument not defined!"
3637
3638        if portfolio is None or not portfolio:
3639            portfolio = self.Overview(show=False)
3640
3641        if self._ticker:
3642            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3643            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3644
3645            for instrument in portfolio["stat"]["orders"]:
3646                if instrument["ticker"] == self._ticker:
3647                    result = True
3648                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3649                    break
3650
3651        elif self._figi:
3652            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3653            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3654
3655            for instrument in portfolio["stat"]["orders"]:
3656                if instrument["figi"] == self._figi:
3657                    result = True
3658                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3659                    break
3660
3661        else:
3662            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3663
3664        uLogger.debug(msg)
3665
3666        return result
3667
3668    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3669        """
3670        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3671        Instrument must be defined by `ticker` (highly priority) or `figi`.
3672
3673        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3674
3675        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3676        :return: list with `orderID`s of limit orders.
3677        """
3678        result = []
3679        msg = "Instrument not defined!"
3680
3681        if portfolio is None or not portfolio:
3682            portfolio = self.Overview(show=False)
3683
3684        if self._ticker:
3685            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3686            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3687
3688            for instrument in portfolio["stat"]["orders"]:
3689                if instrument["ticker"] == self._ticker:
3690                    result.append(instrument["orderID"])
3691
3692            if result:
3693                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3694
3695        elif self._figi:
3696            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3697            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3698
3699            for instrument in portfolio["stat"]["orders"]:
3700                if instrument["figi"] == self._figi:
3701                    result.append(instrument["orderID"])
3702
3703            if result:
3704                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3705
3706        else:
3707            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3708
3709        uLogger.debug(msg)
3710
3711        return result
3712
3713    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3714        """
3715        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3716
3717        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3718
3719        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3720        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3721        """
3722        result = False
3723        msg = "Instrument not defined!"
3724
3725        if portfolio is None or not portfolio:
3726            portfolio = self.Overview(show=False)
3727
3728        if self._ticker:
3729            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3730            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3731
3732            for instrument in portfolio["stat"]["stopOrders"]:
3733                if instrument["ticker"] == self._ticker:
3734                    result = True
3735                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3736                    break
3737
3738        elif self._figi:
3739            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3740            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3741
3742            for instrument in portfolio["stat"]["stopOrders"]:
3743                if instrument["figi"] == self._figi:
3744                    result = True
3745                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3746                    break
3747
3748        else:
3749            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3750
3751        uLogger.debug(msg)
3752
3753        return result
3754
3755    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3756        """
3757        Returns list with all `orderID`s of opened stop orders for the instrument.
3758        Instrument must be defined by `ticker` (highly priority) or `figi`.
3759
3760        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3761
3762        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3763        :return: list with `orderID`s of stop orders.
3764        """
3765        result = []
3766        msg = "Instrument not defined!"
3767
3768        if portfolio is None or not portfolio:
3769            portfolio = self.Overview(show=False)
3770
3771        if self._ticker:
3772            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3773            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3774
3775            for instrument in portfolio["stat"]["stopOrders"]:
3776                if instrument["ticker"] == self._ticker:
3777                    result.append(instrument["orderID"])
3778
3779            if result:
3780                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3781
3782        elif self._figi:
3783            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3784            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3785
3786            for instrument in portfolio["stat"]["stopOrders"]:
3787                if instrument["figi"] == self._figi:
3788                    result.append(instrument["orderID"])
3789
3790            if result:
3791                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3792
3793        else:
3794            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3795
3796        uLogger.debug(msg)
3797
3798        return result
3799
3800    def RequestLimits(self) -> dict:
3801        """
3802        Method for obtaining the available funds for withdrawal for current `accountId`.
3803
3804        See also:
3805        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3806        - `OverviewLimits()` method
3807
3808        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3809                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3810                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3811                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3812        """
3813        if self.accountId is None or not self.accountId:
3814            uLogger.error("Variable `accountId` must be defined for using this method!")
3815            raise Exception("Account ID required")
3816
3817        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3818
3819        self.body = str({"accountId": self.accountId})
3820        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3821        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3822
3823        if self.moreDebug:
3824            uLogger.debug("Records about available funds for withdrawal successfully received")
3825
3826        return rawLimits
3827
3828    def OverviewLimits(self, show: bool = False) -> dict:
3829        """
3830        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3831
3832        See also: `RequestLimits()`.
3833
3834        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3835        :return: dict with raw parsed data from server and some calculated statistics about it.
3836        """
3837        if self.accountId is None or not self.accountId:
3838            uLogger.error("Variable `accountId` must be defined for using this method!")
3839            raise Exception("Account ID required")
3840
3841        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3842
3843        view = {
3844            "rawLimits": rawLimits,
3845            "limits": {  # parsed data for every currency:
3846                "money": {  # this is an array of portfolio currency positions
3847                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3848                },
3849                "blocked": {  # this is an array of blocked currency
3850                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3851                },
3852                "blockedGuarantee": {  # this is locked money under collateral for futures
3853                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3854                },
3855            },
3856        }
3857
3858        # --- Prepare text table with limits in human-readable format:
3859        if show:
3860            info = [
3861                "# Withdrawal limits\n\n",
3862                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3863                "* **Account ID:** [{}]\n".format(self.accountId),
3864            ]
3865
3866            if view["limits"]["money"]:
3867                info.extend([
3868                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3869                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3870                ])
3871
3872            else:
3873                info.append("\nNo withdrawal limits\n")
3874
3875            for curr in view["limits"]["money"].keys():
3876                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3877                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3878                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3879
3880                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3881                    "[{}]".format(curr),
3882                    "{:.2f}".format(view["limits"]["money"][curr]),
3883                    "{:.2f}".format(availableMoney),
3884                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3885                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3886                )
3887
3888                if curr == "rub":
3889                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3890
3891                else:
3892                    info.append(infoStr)
3893
3894            infoText = "".join(info)
3895
3896            uLogger.info(infoText)
3897
3898            if self.withdrawalLimitsFile:
3899                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3900                    fH.write(infoText)
3901
3902                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3903
3904        return view
3905
3906    def RequestAccounts(self) -> dict:
3907        """
3908        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3909
3910        See also:
3911        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3912        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3913        - `OverviewUserInfo()` method
3914
3915        :return: dict with raw data from server that contains accounts info. Example of dict:
3916                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3917                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3918                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3919                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3920        """
3921        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3922
3923        self.body = str({})
3924        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3925        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3926
3927        if self.moreDebug:
3928            uLogger.debug("Records about available accounts successfully received")
3929
3930        return rawAccounts
3931
3932    def RequestUserInfo(self) -> dict:
3933        """
3934        Method for requesting common user's information.
3935
3936        See also:
3937        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3938        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3939        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3940        - `OverviewUserInfo()` method
3941
3942        :return: dict with raw data from server that contains user's information. Example of dict:
3943                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3944                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3945        """
3946        uLogger.debug("Requesting common user's information. Wait, please...")
3947
3948        self.body = str({})
3949        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3950        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3951
3952        if self.moreDebug:
3953            uLogger.debug("Records about current user successfully received")
3954
3955        return rawUserInfo
3956
3957    def RequestMarginStatus(self, accountId: str = None) -> dict:
3958        """
3959        Method for requesting margin calculation for defined account ID.
3960
3961        See also:
3962        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3963        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3964        - `OverviewUserInfo()` method
3965
3966        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3967        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3968                 Example of responses:
3969                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3970                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3971                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3972                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3973                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3974                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3975        """
3976        if accountId is None or not accountId:
3977            if self.accountId is None or not self.accountId:
3978                uLogger.error("Variable `accountId` must be defined for using this method!")
3979                raise Exception("Account ID required")
3980
3981            else:
3982                accountId = self.accountId  # use `self.accountId` (main ID) by default
3983
3984        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3985
3986        self.body = str({"accountId": accountId})
3987        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3988        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3989
3990        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3991            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3992            rawMargin = {}
3993
3994        else:
3995            if self.moreDebug:
3996                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3997
3998        return rawMargin
3999
4000    def RequestTariffLimits(self) -> dict:
4001        """
4002        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4003
4004        See also:
4005        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4006        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4007        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4008        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4009        - `OverviewUserInfo()` method
4010
4011        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4012                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4013                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4014        """
4015        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4016
4017        self.body = str({})
4018        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4019        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4020
4021        if self.moreDebug:
4022            uLogger.debug("Records with limits of current tariff successfully received")
4023
4024        return rawTariffLimits
4025
4026    def RequestBondCoupons(self, iJSON: dict) -> dict:
4027        """
4028        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4029        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4030        All dates are in UTC timezone.
4031
4032        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4033        Documentation:
4034        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4035        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4036
4037        See also: `ExtendBondsData()`.
4038
4039        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4040                      If raw iJSON is not data of bond then server returns an error [400] with message:
4041                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4042        :return: dictionary with bond payment calendar. Response example
4043                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4044                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4045                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4046                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4047        """
4048        if iJSON["figi"] is None or not iJSON["figi"]:
4049            uLogger.error("FIGI must be defined for using this method!")
4050            raise Exception("FIGI required")
4051
4052        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4053        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4054
4055        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4056            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4057            self._figi,
4058            startDate,
4059            endDate,
4060        ))
4061
4062        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4063        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4064        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4065
4066        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4067            uLogger.warning("Instrument type is not bond!")
4068
4069        else:
4070            if self.moreDebug:
4071                uLogger.debug("Records about bond payment calendar successfully received")
4072
4073        return calendar
4074
4075    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4076        """
4077        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4078        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4079        coupon yields, current yields and some statistics etc.
4080
4081        WARNING! This is too long operation if a lot of bonds requested from broker server.
4082
4083        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4084
4085        :param instruments: list of strings with tickers or FIGIs.
4086        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4087                     for further used by data scientists or stock analytics.
4088        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4089                 In XLSX-file and Pandas DataFrame fields mean:
4090                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4091                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4092        """
4093        if instruments is None or not instruments:
4094            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4095            raise Exception("Ticker or FIGI required")
4096
4097        if isinstance(instruments, str):
4098            instruments = [instruments]
4099
4100        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4101
4102        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4103
4104        iCount = len(uniqueInstruments)
4105        tooLong = iCount >= 20
4106        if tooLong:
4107            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4108
4109        bonds = None
4110        for i, self._figi in enumerate(uniqueInstruments):
4111            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4112
4113            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4114                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4115                rawBond = self.SearchByFIGI(requestPrice=True)
4116
4117                # Widen raw data with UTC current time (iData["actualDateTime"]):
4118                actualDate = datetime.now(tzutc())
4119                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4120
4121                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4122                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4123
4124                # Replace some values with human-readable:
4125                iData["nominalCurrency"] = iData["nominal"]["currency"]
4126                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4127                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4128                iData["aciCurrency"] = iData["aciValue"]["currency"]
4129                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4130                iData["issueSize"] = int(iData["issueSize"])
4131                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4132                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4133                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4134                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4135                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4136                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4137                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4138                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4139                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4140                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4141
4142                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4143                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4144                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4145                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4146                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4147                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4148                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4149                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4150                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4151                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4152                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4153
4154                # Widen raw data with calendar data from `rawCalendar` values:
4155                calendarData = []
4156                if "events" in iData["rawCalendar"].keys():
4157                    for item in iData["rawCalendar"]["events"]:
4158                        calendarData.append({
4159                            "couponDate": item["couponDate"],
4160                            "couponNumber": int(item["couponNumber"]),
4161                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4162                            "payCurrency": item["payOneBond"]["currency"],
4163                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4164                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4165                            "couponStartDate": item["couponStartDate"],
4166                            "couponEndDate": item["couponEndDate"],
4167                            "couponPeriod": item["couponPeriod"],
4168                        })
4169
4170                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4171                    if "maturityDate" not in iData.keys():
4172                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4173
4174                # Widen raw data with Coupon Rate.
4175                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4176                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4177                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4178                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4179
4180                # Widen raw data with Yield to Maturity (YTM) on current date.
4181                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4182                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4183                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4184                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4185                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4186                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4187
4188                iData["calendar"] = calendarData  # adds calendar at the end
4189
4190                # Remove not used data:
4191                iData.pop("uid")
4192                iData.pop("positionUid")
4193                iData.pop("currentPrice")
4194                iData.pop("rawCalendar")
4195
4196                colNames = list(iData.keys())
4197                if bonds is None:
4198                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4199
4200                else:
4201                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4202
4203            else:
4204                uLogger.warning("Instrument is not a bond!")
4205
4206            processed = round(100 * (i + 1) / iCount, 1)
4207            if tooLong and processed % 5 == 0:
4208                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4209
4210            else:
4211                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4212
4213        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4214
4215        # Saving bonds from Pandas DataFrame to XLSX sheet:
4216        if xlsx and self.bondsXLSXFile:
4217            with pd.ExcelWriter(
4218                    path=self.bondsXLSXFile,
4219                    date_format=TKS_DATE_FORMAT,
4220                    datetime_format=TKS_DATE_TIME_FORMAT,
4221                    mode="w",
4222            ) as writer:
4223                bonds.to_excel(
4224                    writer,
4225                    sheet_name="Extended bonds data",
4226                    index=True,
4227                    encoding="UTF-8",
4228                    freeze_panes=(1, 1),
4229                )  # saving as XLSX-file with freeze first row and column as headers
4230
4231            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4232
4233        return bonds
4234
4235    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4236        """
4237        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4238
4239        WARNING! This is too long operation if a lot of bonds requested from broker server.
4240
4241        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4242
4243        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4244                        extended information about bonds: main info, current prices, bond payment calendar,
4245                        coupon yields, current yields and some statistics etc.
4246                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4247        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4248                     for further used by data scientists or stock analytics.
4249        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4250        """
4251        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4252            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4253
4254        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4255
4256        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4257        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4258        calendar = None
4259        for bond in extBonds.iterrows():
4260            for item in bond[1]["calendar"]:
4261                cData = {
4262                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4263                    "couponDate": item["couponDate"],
4264                    "figi": bond[1]["figi"],
4265                    "ticker": bond[1]["ticker"],
4266                    "name": bond[1]["name"],
4267                    "couponNumber": item["couponNumber"],
4268                    "payOneBond": item["payOneBond"],
4269                    "payCurrency": item["payCurrency"],
4270                    "couponType": item["couponType"],
4271                    "couponPeriod": item["couponPeriod"],
4272                    "fixDate": item["fixDate"],
4273                    "couponStartDate": item["couponStartDate"],
4274                    "couponEndDate": item["couponEndDate"],
4275                }
4276
4277                if calendar is None:
4278                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4279
4280                else:
4281                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4282
4283        if calendar is not None:
4284            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4285
4286            # Saving calendar from Pandas DataFrame to XLSX sheet:
4287            if xlsx:
4288                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4289
4290                with pd.ExcelWriter(
4291                        path=xlsxCalendarFile,
4292                        date_format=TKS_DATE_FORMAT,
4293                        datetime_format=TKS_DATE_TIME_FORMAT,
4294                        mode="w",
4295                ) as writer:
4296                    humanReadable = calendar.copy(deep=True)
4297                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4298                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4299                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4300                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4301                    humanReadable.columns = colNames  # human-readable column names
4302
4303                    humanReadable.to_excel(
4304                        writer,
4305                        sheet_name="Bond payments calendar",
4306                        index=False,
4307                        encoding="UTF-8",
4308                        freeze_panes=(1, 2),
4309                    )  # saving as XLSX-file with freeze first row and column as headers
4310
4311                    del humanReadable  # release df in memory
4312
4313                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4314
4315        return calendar
4316
4317    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4318        """
4319        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4320        Also, creates Markdown file with calendar data, `calendar.md` by default.
4321
4322        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4323
4324        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4325                        extended information about bonds: main info, current prices, bond payment calendar,
4326                        coupon yields, current yields and some statistics etc.
4327                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4328        :param show: if `True` then also printing bonds payment calendar to the console,
4329                     otherwise save to file `calendarFile` only. `False` by default.
4330        :return: multilines text in Markdown format with bonds payment calendar as a table.
4331        """
4332        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4333            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4334
4335        infoText = "# Bond payments calendar\n\n"
4336
4337        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4338
4339        if not (calendar is None or calendar.empty):
4340            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4341
4342            info = [
4343                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4344                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4345            ]
4346
4347            newMonth = False
4348            notOneBond = calendar["figi"].nunique() > 1
4349            for i, bond in enumerate(calendar.iterrows()):
4350                if newMonth and notOneBond:
4351                    info.append(splitLine)
4352
4353                info.append(
4354                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4355                        "  √" if bond[1]["paid"] else "  —",
4356                        bond[1]["couponDate"].split("T")[0],
4357                        bond[1]["figi"],
4358                        bond[1]["ticker"],
4359                        bond[1]["couponNumber"],
4360                        "{} {}".format(
4361                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4362                            bond[1]["payCurrency"],
4363                        ),
4364                        bond[1]["couponType"],
4365                        bond[1]["couponPeriod"],
4366                        bond[1]["fixDate"].split("T")[0],
4367                    )
4368                )
4369
4370                if i < len(calendar.values) - 1:
4371                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4372                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4373                    newMonth = False if curDate.month == nextDate.month else True
4374
4375                else:
4376                    newMonth = False
4377
4378            infoText += "".join(info)
4379
4380            if show:
4381                uLogger.info("{}".format(infoText))
4382
4383            if self.calendarFile is not None:
4384                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4385                    fH.write(infoText)
4386
4387                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4388
4389        else:
4390            infoText += "No data\n"
4391
4392        return infoText
4393
4394    def OverviewAccounts(self, show: bool = False) -> dict:
4395        """
4396        Method for parsing and show simple table with all available user accounts.
4397
4398        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4399
4400        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4401        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4402                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4403                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4404                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4405                                                        "closed": "—", "access": "Full access" }, ...}}`
4406        """
4407        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4408
4409        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4410        accounts = {
4411            item["id"]: {
4412                "type": TKS_ACCOUNT_TYPES[item["type"]],
4413                "name": item["name"],
4414                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4415                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4416                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4417                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4418            } for item in rawAccounts["accounts"]
4419        }
4420
4421        # Raw and parsed data with some fields replaced in "stat" section:
4422        view = {
4423            "rawAccounts": rawAccounts,
4424            "stat": accounts,
4425        }
4426
4427        # --- Prepare simple text table with only accounts data in human-readable format:
4428        if show:
4429            info = [
4430                "# User accounts\n\n",
4431                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4432                "| Account ID   | Type                      | Status                    | Name                           |\n",
4433                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4434            ]
4435
4436            for account in view["stat"].keys():
4437                info.extend([
4438                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4439                        account,
4440                        view["stat"][account]["type"],
4441                        view["stat"][account]["status"],
4442                        view["stat"][account]["name"],
4443                    )
4444                ])
4445
4446            infoText = "".join(info)
4447
4448            uLogger.info(infoText)
4449
4450            if self.userAccountsFile:
4451                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4452                    fH.write(infoText)
4453
4454                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4455
4456        return view
4457
4458    def OverviewUserInfo(self, show: bool = False) -> dict:
4459        """
4460        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4461
4462        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4463
4464        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4465        :return: dict with raw parsed data from server and some calculated statistics about it.
4466        """
4467        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4468        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4469        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4470        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4471        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4472        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4473
4474        # This is dict with parsed common user data:
4475        userInfo = {
4476            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4477            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4478            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4479            "tariff": rawUserInfo["tariff"],
4480        }
4481
4482        # This is an array of dict with parsed margin statuses for every account IDs:
4483        margins = {}
4484        for accountId in accounts.keys():
4485            if rawMargins[accountId]:
4486                margins[accountId] = {
4487                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4488                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4489                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4490                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4491                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4492                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4493                }
4494
4495            else:
4496                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4497
4498        unary = {}  # unary-connection limits
4499        for item in rawTariffLimits["unaryLimits"]:
4500            if item["limitPerMinute"] in unary.keys():
4501                unary[item["limitPerMinute"]].extend(item["methods"])
4502
4503            else:
4504                unary[item["limitPerMinute"]] = item["methods"]
4505
4506        stream = {}  # stream-connection limits
4507        for item in rawTariffLimits["streamLimits"]:
4508            if item["limit"] in stream.keys():
4509                stream[item["limit"]].extend(item["streams"])
4510
4511            else:
4512                stream[item["limit"]] = item["streams"]
4513
4514        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4515        limits = {
4516            "unary": unary,
4517            "stream": stream,
4518        }
4519
4520        # Raw and parsed data as an output result:
4521        view = {
4522            "rawUserInfo": rawUserInfo,
4523            "rawAccounts": rawAccounts,
4524            "rawMargins": rawMargins,
4525            "rawTariffLimits": rawTariffLimits,
4526            "stat": {
4527                "userInfo": userInfo,
4528                "accounts": accounts,
4529                "margins": margins,
4530                "limits": limits,
4531            },
4532        }
4533
4534        # --- Prepare text table with user information in human-readable format:
4535        if show:
4536            info = [
4537                "# Full user information\n\n",
4538                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4539                "## Common information\n\n",
4540                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4541                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4542                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4543                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4544                "\n## User accounts\n\n",
4545            ]
4546
4547            for account in view["stat"]["accounts"].keys():
4548                info.extend([
4549                    "### ID: [{}]\n\n".format(account),
4550                    "| Parameters           | Values                                                       |\n",
4551                    "|----------------------|--------------------------------------------------------------|\n",
4552                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4553                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4554                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4555                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4556                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4557                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4558                ])
4559
4560                if margins[account]:
4561                    info.extend([
4562                        "| Margin status:       | Enabled                                                      |\n",
4563                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4564                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4565                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4566                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4567                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4568                    ])
4569
4570                else:
4571                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4572
4573            info.extend([
4574                "\n## Current user tariff limits\n",
4575                "\nSee also:\n",
4576                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4577                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4578                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4579                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4580                "\n### Unary limits\n",
4581            ])
4582
4583            if unary:
4584                for key, values in sorted(unary.items()):
4585                    info.append("\n* Max requests per minute: {}\n".format(key))
4586
4587                    for value in values:
4588                        info.append("  - {}\n".format(value))
4589
4590            else:
4591                info.append("\nNot available\n")
4592
4593            info.append("\n### Stream limits\n")
4594
4595            if stream:
4596                for key, values in sorted(stream.items()):
4597                    info.append("\n* Max stream connections: {}\n".format(key))
4598
4599                    for value in values:
4600                        info.append("  - {}\n".format(value))
4601
4602            else:
4603                info.append("\nNot available\n")
4604
4605            infoText = "".join(info)
4606
4607            uLogger.info(infoText)
4608
4609            if self.userInfoFile:
4610                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4611                    fH.write(infoText)
4612
4613                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4614
4615        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
 84    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 85        """
 86        Main class init.
 87
 88        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 89        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 90                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 91        :param useCache: use default cache file with raw data to use instead of `iList`.
 92                         True by default. Cache is auto-update if new day has come.
 93                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 94        :param defaultCache: path to default cache file. `dump.json` by default.
 95        """
 96        if token is None or not token:
 97            try:
 98                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 99                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
100
101            except KeyError:
102                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
103                raise Exception("Token required")
104
105        else:
106            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
107            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
108
109        if accountId is None or not accountId:
110            try:
111                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
112                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
113
114            except KeyError:
115                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
116
117        else:
118            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
119            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
120
121        self.version = __version__  # duplicate here used TKSBrokerAPI main version
122        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
123
124        Latest version: https://pypi.org/project/tksbrokerapi/
125        """
126
127        self.aliases = TKS_TICKER_ALIASES
128        """Some aliases instead official tickers.
129
130        See also: `TKSEnums.TKS_TICKER_ALIASES`
131        """
132
133        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
134
135        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
136
137        self._ticker = ""
138        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
139
140        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
141        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
142
143        See also: `SearchByTicker()`, `SearchInstruments()`.
144        """
145
146        self._figi = ""
147        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
148
149        See also: `SearchByFIGI()`, `SearchInstruments()`.
150        """
151
152        self.depth = 1
153        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
154
155        See also: `GetCurrentPrices()`.
156        """
157
158        self.server = r"https://invest-public-api.tinkoff.ru/rest"
159        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
160
161        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
162        """
163
164        uLogger.debug("Broker API server: {}".format(self.server))
165
166        self.timeout = 15
167        """Server operations timeout in seconds. Default: `15`.
168
169        See also: `SendAPIRequest()`.
170        """
171
172        self.headers = {
173            "Content-Type": "application/json",
174            "accept": "application/json",
175            "Authorization": "Bearer {}".format(self.token),
176            "x-app-name": "Tim55667757.TKSBrokerAPI",
177        }
178        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
179
180        See also: `SendAPIRequest()`.
181        """
182
183        self.body = None
184        """Request body which send to broker server. Default: `None`.
185
186        See also: `SendAPIRequest()`.
187        """
188
189        self.moreDebug = False
190        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
191
192        self.historyFile = None
193        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
194
195        See also: `History()`.
196        """
197
198        self.htmlHistoryFile = "index.html"
199        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
200
201        See also: `ShowHistoryChart()`.
202        """
203
204        self.instrumentsFile = "instruments.md"
205        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
206
207        See also: `ShowInstrumentsInfo()`.
208        """
209
210        self.searchResultsFile = "search-results.md"
211        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
212
213        See also: `SearchInstruments()`.
214        """
215
216        self.pricesFile = "prices.md"
217        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
218
219        See also: `GetListOfPrices()`.
220        """
221
222        self.infoFile = "info.md"
223        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
224
225        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
226        """
227
228        self.bondsXLSXFile = "ext-bonds.xlsx"
229        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
230        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
231
232        See also: `ExtendBondsData()`.
233        """
234
235        self.calendarFile = "calendar.md"
236        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
237        
238        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
239
240        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
241        """
242
243        self.overviewFile = "overview.md"
244        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
245
246        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
247        """
248
249        self.overviewDigestFile = "overview-digest.md"
250        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
251
252        See also: `Overview()` with parameter `details="digest"`.
253        """
254
255        self.overviewPositionsFile = "overview-positions.md"
256        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
257
258        See also: `Overview()` with parameter `details="positions"`.
259        """
260
261        self.overviewOrdersFile = "overview-orders.md"
262        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
263
264        See also: `Overview()` with parameter `details="orders"`.
265        """
266
267        self.overviewAnalyticsFile = "overview-analytics.md"
268        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
269
270        See also: `Overview()` with parameter `details="analytics"`.
271        """
272
273        self.overviewBondsCalendarFile = "overview-calendar.md"
274        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
275
276        See also: `Overview()` with parameter `details="calendar"`.
277        """
278
279        self.reportFile = "deals.md"
280        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
281
282        See also: `Deals()`.
283        """
284
285        self.withdrawalLimitsFile = "limits.md"
286        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
287
288        See also: `OverviewLimits()` and `RequestLimits()`.
289        """
290
291        self.userInfoFile = "user-info.md"
292        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
293
294        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
295        """
296
297        self.userAccountsFile = "accounts.md"
298        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
299
300        See also: `OverviewAccounts()`, `RequestAccounts()`.
301        """
302
303        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
304        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
305
306        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
307
308        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
309        """
310
311        self.iList = None  # init iList for raw instruments data
312        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
313        
314        See also: `Listing()`, `DumpInstruments()`.
315        """
316
317        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
318        if useCache:
319            if os.path.exists(self.iListDumpFile):
320                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
321                curTime = datetime.now(tzutc())
322
323                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
324                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
325
326                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
327
328                else:
329                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
330
331                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
332                        os.path.abspath(self.iListDumpFile),
333                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
334                    ))
335
336            else:
337                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
338                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
339
340        else:
341            self.iList = self.Listing()  # request new raw instruments data from broker server
342            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
343
344        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
345        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
346
347        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
348        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

moreDebug

Enables more debug information in this class, such as net request and response headers in all methods. False by default.

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

overviewBondsCalendarFile

Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.

See also: Overview() with parameter details="calendar".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

ticker: str

String with ticker, e.g. GOOGL. Tickers may be upper case only.

Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi: str

String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.

See also: SearchByFIGI(), SearchInstruments().

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5) -> dict:
402    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
403        """
404        Send GET or POST request to broker server and receive JSON object.
405
406        self.header: must be defining with dictionary of headers.
407        self.body: if define then used as request body. None by default.
408        self.timeout: global request timeout, 15 seconds by default.
409        :param url: url with REST request.
410        :param reqType: send "GET" or "POST" request. "GET" by default.
411        :param retry: how many times retry after first request if an 5xx server errors occurred.
412        :param pause: sleep time in seconds between retries.
413        :return: response JSON (dictionary) from broker.
414        """
415        if reqType.upper() not in ("GET", "POST"):
416            uLogger.error("You can define request type: `GET` or `POST`!")
417            raise Exception("Incorrect value")
418
419        if self.moreDebug:
420            uLogger.debug("Request parameters:")
421            uLogger.debug("    - REST API URL: {}".format(url))
422            uLogger.debug("    - request type: {}".format(reqType))
423            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
424            uLogger.debug("    - body:\n{}".format(self.body))
425
426        # fast hack to avoid all operations with some tickers/FIGI
427        responseJSON = {}
428        oK = True
429        for item in self.exclude:
430            if item in url:
431                if self.moreDebug:
432                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
433
434                oK = False
435                break
436
437        if oK:
438            counter = 0
439            response = None
440            errMsg = ""
441
442            while not response and counter <= retry:
443                if reqType == "GET":
444                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
445
446                if reqType == "POST":
447                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
448
449                if self.moreDebug:
450                    uLogger.debug("Response:")
451                    uLogger.debug("    - status code: {}".format(response.status_code))
452                    uLogger.debug("    - reason: {}".format(response.reason))
453                    uLogger.debug("    - body length: {}".format(len(response.text)))
454                    uLogger.debug("    - headers:\n{}".format(response.headers))
455
456                # Server returns some headers:
457                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
458                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
459                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
460                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
461                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
462                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
463                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
464                    sleep(rateLimitWait)
465
466                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
467                if 400 <= response.status_code < 500:
468                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
469                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
470
471                    if "code" in response.text and "message" in response.text:
472                        msgDict = self._ParseJSON(rawData=response.text)
473                        uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
474
475                    counter = retry + 1  # do not retry for 4xx errors
476
477                if 500 <= response.status_code < 600:
478                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
479                    uLogger.debug("    - not oK, {}".format(errMsg))
480
481                    if "code" in response.text and "message" in response.text:
482                        errMsgDict = self._ParseJSON(rawData=response.text)
483                        uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
484
485                    counter += 1
486
487                    if counter <= retry:
488                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
489                        sleep(pause)
490
491            responseJSON = self._ParseJSON(rawData=response.text)
492
493            if errMsg:
494                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
495                uLogger.error("    - not oK, {}".format(errMsg))
496
497        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
530    def Listing(self) -> dict:
531        """
532        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
533
534        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
535        """
536        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
537        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
538
539        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
540        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
541        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
542
543        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
544        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
545        poolUpdater.close()
546
547        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
548        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
549        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
550
551        # calculate minimum price increment (step) for all instruments and set up instrument's type:
552        for iType in iList.keys():
553            for ticker in iList[iType]:
554                iList[iType][ticker]["type"] = iType
555
556                if "minPriceIncrement" in iList[iType][ticker].keys():
557                    iList[iType][ticker]["step"] = NanoToFloat(
558                        iList[iType][ticker]["minPriceIncrement"]["units"],
559                        iList[iType][ticker]["minPriceIncrement"]["nano"],
560                    )
561
562                else:
563                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
564
565        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
567    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
568        """
569        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
570
571        See also: `DumpInstruments()`, `Listing()`.
572
573        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
574                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
575        """
576        if self.iListDumpFile is None or not self.iListDumpFile:
577            uLogger.error("Output name of dump file must be defined!")
578            raise Exception("Filename required")
579
580        if not self.iList or forceUpdate:
581            self.iList = self.Listing()
582
583        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
584
585        # Save as XLSX with separated sheets for every type of instruments:
586        with pd.ExcelWriter(
587                path=xlsxDumpFile,
588                date_format=TKS_DATE_FORMAT,
589                datetime_format=TKS_DATE_TIME_FORMAT,
590                mode="w",
591        ) as writer:
592            for iType in TKS_INSTRUMENTS:
593                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
594                df = df[sorted(df)]  # sorted by column names
595                df = df.applymap(
596                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
597                    na_action="ignore",
598                )  # converting numbers from nano-type to float in every cell
599                df.to_excel(
600                    writer,
601                    sheet_name=iType,
602                    encoding="UTF-8",
603                    freeze_panes=(1, 1),
604                )  # saving as XLSX-file with freeze first row and column as headers
605
606        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
608    def DumpInstruments(self, forceUpdate: bool = True) -> str:
609        """
610        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
611        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
612
613        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
614
615        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
616                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
617        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
618        """
619        if self.iListDumpFile is None or not self.iListDumpFile:
620            uLogger.error("Output name of dump file must be defined!")
621            raise Exception("Filename required")
622
623        if not self.iList or forceUpdate:
624            self.iList = self.Listing()
625
626        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
627        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
628            fH.write(jsonDump)
629
630        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
631
632        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
634    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
635        """
636        Show information about one instrument defined by json data and prints it in Markdown format.
637
638        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
639
640        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
641        :param show: if `True` then also printing information about instrument and its current price.
642        :return: multilines text in Markdown format with information about one instrument.
643        """
644        splitLine = "|                                                             |                                                        |\n"
645        infoText = ""
646
647        if iJSON is not None and iJSON and isinstance(iJSON, dict):
648            info = [
649                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
650                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
651                "| Parameters                                                  | Values                                                 |\n",
652                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
653                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
654                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
655            ]
656
657            if "sector" in iJSON.keys() and iJSON["sector"]:
658                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
659
660            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
661                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
662
663            info.extend([
664                splitLine,
665                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
666                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
667            ])
668
669            if "isin" in iJSON.keys() and iJSON["isin"]:
670                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
671
672            if "classCode" in iJSON.keys():
673                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
674
675            info.extend([
676                splitLine,
677                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
678                splitLine,
679                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
680                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
681                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
682            ])
683
684            if iJSON["figi"]:
685                self._figi = iJSON["figi"]
686                iJSON = iJSON | self.RequestTradingStatus()
687
688                info.extend([
689                    splitLine,
690                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
691                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
692                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
693                ])
694
695            info.append(splitLine)
696
697            if "type" in iJSON.keys() and iJSON["type"]:
698                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
699
700                if "shareType" in iJSON.keys() and iJSON["shareType"]:
701                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
702
703            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
704                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
705
706            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
707                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
708
709            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
710                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
711
712            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
713                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
714
715            if "focusType" in iJSON.keys() and iJSON["focusType"]:
716                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
717
718            if "assetType" in iJSON.keys() and iJSON["assetType"]:
719                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
720
721            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
722                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
723
724            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
725                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
726
727            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
728                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
729
730            if "currency" in iJSON.keys():
731                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
732
733            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
734                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
735
736            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
737                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
738
739            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
740                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
741
742            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
743                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
744
745            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
746                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
747
748            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
749                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
750
751            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
752                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
753
754            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
755                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
756
757            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
758                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
759
760            iExt = None
761            if iJSON["type"] == "Bonds":
762                info.extend([
763                    splitLine,
764                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
765                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
766                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
767                        iJSON["nominal"]["currency"],
768                    )),
769                ])
770
771                if "floatingCouponFlag" in iJSON.keys():
772                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
773
774                if "amortizationFlag" in iJSON.keys():
775                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
776
777                info.append(splitLine)
778
779                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
780                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
781
782                if iJSON["figi"]:
783                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
784
785                    info.extend([
786                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
787                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
788                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
789                    ])
790
791                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
792                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
793                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
794                        iJSON["aciValue"]["currency"]
795                    )))
796
797            if "currentPrice" in iJSON.keys():
798                info.append(splitLine)
799
800                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
801                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
802
803                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
804                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
805                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
806                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
807                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
808
809                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
810                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
811
812                info.extend([
813                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
814                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
815                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
816                    )),
817                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
818                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
819                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
820                    )),
821                    "| Changes between last deal price and last close              | {:<54} |\n".format(
822                        "{:.2f}%{}".format(
823                            iJSON["currentPrice"]["changes"],
824                            " ({}{:.2f} {})".format(
825                                "+" if bondChangesDelta > 0 else "",
826                                bondChangesDelta,
827                                aciCurrency
828                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
829                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
830                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
831                                currency
832                            ),
833                        )
834                    ),
835                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
836                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
837                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
838                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
839                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
840                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
841                    )),
842                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
843                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
844                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
845                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
846                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
847                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
848                    )),
849                ])
850
851            if "lot" in iJSON.keys():
852                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
853
854            if "step" in iJSON.keys() and iJSON["step"] != 0:
855                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
856
857            # Add bond payment calendar:
858            if iJSON["type"] == "Bonds":
859                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
860                info.extend(["\n", strCalendar])
861
862            infoText += "".join(info)
863
864            if show:
865                uLogger.info("{}".format(infoText))
866
867            else:
868                uLogger.debug("{}".format(infoText))
869
870            if self.infoFile is not None:
871                with open(self.infoFile, "w", encoding="UTF-8") as fH:
872                    fH.write(infoText)
873
874                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
875
876        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self._ticker]
  • show: if True then also printing information about instrument and its current price.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
878    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
879        """
880        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
881
882        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
883        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
884        :return: JSON formatted data with information about instrument.
885        """
886        tickerJSON = {}
887        if self.moreDebug:
888            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
889
890        if not self._ticker:
891            uLogger.warning("self._ticker variable is not be empty!")
892
893        else:
894            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
895                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
896                raise Exception("Instrument not allowed")
897
898            if not self.iList:
899                self.iList = self.Listing()
900
901            if self._ticker in self.iList["Shares"].keys():
902                tickerJSON = self.iList["Shares"][self._ticker]
903                if self.moreDebug:
904                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
905
906            elif self._ticker in self.iList["Currencies"].keys():
907                tickerJSON = self.iList["Currencies"][self._ticker]
908                if self.moreDebug:
909                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
910
911            elif self._ticker in self.iList["Bonds"].keys():
912                tickerJSON = self.iList["Bonds"][self._ticker]
913                if self.moreDebug:
914                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
915
916            elif self._ticker in self.iList["Etfs"].keys():
917                tickerJSON = self.iList["Etfs"][self._ticker]
918                if self.moreDebug:
919                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
920
921            elif self._ticker in self.iList["Futures"].keys():
922                tickerJSON = self.iList["Futures"][self._ticker]
923                if self.moreDebug:
924                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
925
926        if tickerJSON:
927            self._figi = tickerJSON["figi"]
928
929            if requestPrice:
930                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
931
932                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
933                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
934
935                else:
936                    tickerJSON["currentPrice"]["changes"] = 0
937
938            if show:
939                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
940
941        else:
942            if show:
943                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
944
945        return tickerJSON

Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 947    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 948        """
 949        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 950
 951        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 952        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 953        :return: JSON formatted data with information about instrument.
 954        """
 955        figiJSON = {}
 956        if self.moreDebug:
 957            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 958
 959        if not self._figi:
 960            uLogger.warning("self._figi variable is not be empty!")
 961
 962        else:
 963            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 964                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 965                raise Exception("Instrument not allowed")
 966
 967            if not self.iList:
 968                self.iList = self.Listing()
 969
 970            for item in self.iList["Shares"].keys():
 971                if self._figi == self.iList["Shares"][item]["figi"]:
 972                    figiJSON = self.iList["Shares"][item]
 973
 974                    if self.moreDebug:
 975                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
 976
 977                    break
 978
 979            if not figiJSON:
 980                for item in self.iList["Currencies"].keys():
 981                    if self._figi == self.iList["Currencies"][item]["figi"]:
 982                        figiJSON = self.iList["Currencies"][item]
 983
 984                        if self.moreDebug:
 985                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
 986
 987                        break
 988
 989            if not figiJSON:
 990                for item in self.iList["Bonds"].keys():
 991                    if self._figi == self.iList["Bonds"][item]["figi"]:
 992                        figiJSON = self.iList["Bonds"][item]
 993
 994                        if self.moreDebug:
 995                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
 996
 997                        break
 998
 999            if not figiJSON:
1000                for item in self.iList["Etfs"].keys():
1001                    if self._figi == self.iList["Etfs"][item]["figi"]:
1002                        figiJSON = self.iList["Etfs"][item]
1003
1004                        if self.moreDebug:
1005                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1006
1007                        break
1008
1009            if not figiJSON:
1010                for item in self.iList["Futures"].keys():
1011                    if self._figi == self.iList["Futures"][item]["figi"]:
1012                        figiJSON = self.iList["Futures"][item]
1013
1014                        if self.moreDebug:
1015                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1016
1017                        break
1018
1019        if figiJSON:
1020            self._figi = figiJSON["figi"]
1021            self._ticker = figiJSON["ticker"]
1022
1023            if requestPrice:
1024                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1025
1026                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1027                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1028
1029                else:
1030                    figiJSON["currentPrice"]["changes"] = 0
1031
1032            if show:
1033                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1034
1035        else:
1036            if show:
1037                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1038
1039        return figiJSON

Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1041    def GetCurrentPrices(self, show: bool = True) -> dict:
1042        """
1043        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1044        `{"buy": [{"price": 1243.8, "quantity": 193},
1045                  {"price": 1244.0, "quantity": 168},
1046                  {"price": 1244.8, "quantity": 5},
1047                  {"price": 1245.0, "quantity": 61},
1048                  {"price": 1245.4, "quantity": 60}],
1049          "sell": [{"price": 1243.6, "quantity": 8},
1050                   {"price": 1242.6, "quantity": 10},
1051                   {"price": 1242.4, "quantity": 18},
1052                   {"price": 1242.2, "quantity": 50},
1053                   {"price": 1242.0, "quantity": 113}],
1054          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1055        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1056        - sell: list of dicts with Buyers prices,
1057            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1058            - quantity: volume value by current price in lots,
1059        - limitUp: current trade session limit price, maximum,
1060        - limitDown: current trade session limit price, minimum,
1061        - lastPrice: last deal price of the instrument,
1062        - closePrice: previous trade session close price of the instrument.
1063
1064        See also: `SearchByTicker()` and `SearchByFIGI()`.
1065        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1066        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1067
1068        :param show: if `True` then print DOM to log and console.
1069        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1070                 If an error occurred then returns an empty record:
1071                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1072        """
1073        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1074
1075        if self.depth < 1:
1076            uLogger.error("Depth of Market (DOM) must be >=1!")
1077            raise Exception("Incorrect value")
1078
1079        if not (self._ticker or self._figi):
1080            uLogger.error("self._ticker or self._figi variables must be defined!")
1081            raise Exception("Ticker or FIGI required")
1082
1083        if self._ticker and not self._figi:
1084            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1085            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1086
1087        if not self._ticker and self._figi:
1088            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1089            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1090
1091        if not self._figi:
1092            uLogger.error("FIGI is not defined!")
1093            raise Exception("Ticker or FIGI required")
1094
1095        else:
1096            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1097
1098            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1099            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1100            self.body = str({"figi": self._figi, "depth": self.depth})
1101            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1102
1103            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1104                # list of dicts with sellers orders:
1105                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1106
1107                # list of dicts with buyers orders:
1108                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1109
1110                # max price of instrument at this time:
1111                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1112
1113                # min price of instrument at this time:
1114                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1115
1116                # last price of deal with instrument:
1117                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1118
1119                # last close price of instrument:
1120                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1121
1122            else:
1123                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1124                uLogger.debug("Server response: {}".format(pricesResponse))
1125
1126            if show:
1127                if prices["buy"] or prices["sell"]:
1128                    info = [
1129                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1130                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1131                            self._ticker,
1132                            self._figi,
1133                            self.depth,
1134                        ),
1135                        "-" * 60, "\n",
1136                        "             Orders of Buyers | Orders of Sellers\n",
1137                        "-" * 60, "\n",
1138                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1139                        "-" * 60, "\n",
1140                    ]
1141
1142                    if not prices["buy"]:
1143                        info.append("                              | No orders!\n")
1144                        sumBuy = 0
1145
1146                    else:
1147                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1148                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1149                        for item in maxMinSorted:
1150                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1151
1152                    if not prices["sell"]:
1153                        info.append("No orders!                    |\n")
1154                        sumSell = 0
1155
1156                    else:
1157                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1158                        for item in prices["sell"]:
1159                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1160
1161                    info.extend([
1162                        "-" * 60, "\n",
1163                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1164                        "-" * 60, "\n",
1165                    ])
1166
1167                    infoText = "".join(info)
1168
1169                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1170
1171                else:
1172                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1173
1174        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True) -> str:
1176    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1177        """
1178        This method get and show information about all available broker instruments for current user account.
1179        If `instrumentsFile` string is not empty then also save information to this file.
1180
1181        :param show: if `True` then print results to console, if `False` — print only to file.
1182        :return: multi-lines string with all available broker instruments
1183        """
1184        if not self.iList:
1185            self.iList = self.Listing()
1186
1187        info = [
1188            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1189            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1190        ]
1191
1192        # add instruments count by type:
1193        for iType in self.iList.keys():
1194            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1195
1196        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1197        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1198
1199        # generating info tables with all instruments by type:
1200        for iType in self.iList.keys():
1201            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1202
1203            for instrument in self.iList[iType].keys():
1204                iName = self.iList[iType][instrument]["name"]  # instrument's name
1205                if len(iName) > 57:
1206                    iName = "{}...".format(iName[:54])  # right trim for a long string
1207
1208                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1209                    self.iList[iType][instrument]["ticker"],
1210                    iName,
1211                    self.iList[iType][instrument]["figi"],
1212                    self.iList[iType][instrument]["currency"],
1213                    self.iList[iType][instrument]["lot"],
1214                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1215                ))
1216
1217        infoText = "".join(info)
1218
1219        if show:
1220            uLogger.info(infoText)
1221
1222        if self.instrumentsFile:
1223            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1224                fH.write(infoText)
1225
1226            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1227
1228        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False — print only to file.
Returns

multi-lines string with all available broker instruments

def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1230    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1231        """
1232        This method search and show information about instruments by part of its ticker, FIGI or name.
1233        If `searchResultsFile` string is not empty then also save information to this file.
1234
1235        :param pattern: string with part of ticker, FIGI or instrument's name.
1236        :param show: if `True` then print results to console, if `False` — return list of result only.
1237        :return: list of dictionaries with all found instruments.
1238        """
1239        if not self.iList:
1240            self.iList = self.Listing()
1241
1242        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1243        compiledPattern = re.compile(pattern, re.IGNORECASE)
1244
1245        for iType in self.iList:
1246            for instrument in self.iList[iType].values():
1247                searchResult = compiledPattern.search(" ".join(
1248                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1249                ))
1250
1251                if searchResult:
1252                    searchResults[iType][instrument["ticker"]] = instrument
1253
1254        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1255        info = [
1256            "# Search results\n\n",
1257            "* **Search pattern:** [{}]\n".format(pattern),
1258            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1259            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1260        ]
1261        infoShort = info[:]
1262
1263        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1264        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1265        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1266
1267        if resultsLen == 0:
1268            info.append("\nNo results\n")
1269            infoShort.append("\nNo results\n")
1270            uLogger.warning("No results. Try changing your search pattern.")
1271
1272        else:
1273            for iType in searchResults:
1274                iTypeValuesCount = len(searchResults[iType].values())
1275                if iTypeValuesCount > 0:
1276                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1277                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1278
1279                    for instrument in searchResults[iType].values():
1280                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1281                            instrument["type"],
1282                            instrument["ticker"],
1283                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1284                            instrument["figi"],
1285                        ))
1286
1287                    if iTypeValuesCount <= 5:
1288                        infoShort.extend(info[-iTypeValuesCount:])
1289
1290                    else:
1291                        infoShort.extend(info[-5:])
1292                        infoShort.append(skippedLine)
1293
1294        infoText = "".join(info)
1295        infoTextShort = "".join(infoShort)
1296
1297        if show:
1298            uLogger.info(infoTextShort)
1299            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1300
1301        if self.searchResultsFile:
1302            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1303                fH.write(infoText)
1304
1305            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1306
1307        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False — return list of result only.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1309    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1310        """
1311        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1312
1313        :param instruments: list of strings with tickers or FIGIs.
1314        :return: list with unique instrument FIGIs only.
1315        """
1316        requestedInstruments = []
1317        for iName in instruments:
1318            if iName not in self.aliases.keys():
1319                if iName not in requestedInstruments:
1320                    requestedInstruments.append(iName)
1321
1322            else:
1323                if iName not in requestedInstruments:
1324                    if self.aliases[iName] not in requestedInstruments:
1325                        requestedInstruments.append(self.aliases[iName])
1326
1327        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1328
1329        onlyUniqueFIGIs = []
1330        for iName in requestedInstruments:
1331            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1332                continue
1333
1334            self._ticker = iName
1335            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1336
1337            if not iData:
1338                self._ticker = ""
1339                self._figi = iName
1340
1341                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1342
1343                if not iData:
1344                    self._figi = ""
1345                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1346
1347            if iData and iData["figi"] not in onlyUniqueFIGIs:
1348                onlyUniqueFIGIs.append(iData["figi"])
1349
1350        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1351
1352        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1354    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1355        """
1356        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1357
1358        See limits: https://tinkoff.github.io/investAPI/limits/
1359
1360        If `pricesFile` string is not empty then also save information to this file.
1361
1362        :param instruments: list of strings with tickers or FIGIs.
1363        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1364        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1365                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1366        """
1367        if instruments is None or not instruments:
1368            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1369            raise Exception("Ticker or FIGI required")
1370
1371        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1372
1373        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1374
1375        iList = []  # trying to get info and current prices about all unique instruments:
1376        for self._figi in onlyUniqueFIGIs:
1377            iData = self.SearchByFIGI(requestPrice=True)
1378            iList.append(iData)
1379
1380        self.ShowListOfPrices(iList, show)
1381
1382        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!

See limits: https://tinkoff.github.io/investAPI/limits/

If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1384    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1385        """
1386        Show table contains current prices of given instruments.
1387
1388        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1389                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1390        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1391        :return: multilines text in Markdown format as a table contains current prices.
1392        """
1393        infoText = ""
1394
1395        if show or self.pricesFile:
1396            info = [
1397                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1398                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1399                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1400            ]
1401
1402            for item in iList:
1403                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1404                    item["ticker"],
1405                    item["figi"],
1406                    item["type"],
1407                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1408                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1409                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1410                    "{} / {}".format(
1411                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1412                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1413                    ),
1414                    "{} / {}".format(
1415                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1416                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1417                    ),
1418                    item["currency"],
1419                ))
1420
1421            infoText = "".join(info)
1422
1423            if show:
1424                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1425
1426            if self.pricesFile:
1427                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1428                    fH.write(infoText)
1429
1430                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1431
1432        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1434    def RequestTradingStatus(self) -> dict:
1435        """
1436        Requesting trading status for the instrument defined by `figi` variable.
1437
1438        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1439
1440        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1441
1442        :return: dictionary with trading status attributes. Response example:
1443                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1444                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1445        """
1446        if self._figi is None or not self._figi:
1447            uLogger.error("Variable `figi` must be defined for using this method!")
1448            raise Exception("FIGI required")
1449
1450        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1451
1452        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1453        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1454        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1455
1456        if self.moreDebug:
1457            uLogger.debug("Records about current trading status successfully received")
1458
1459        return tradingStatus

Requesting trading status for the instrument defined by figi variable.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus

Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1461    def RequestPortfolio(self) -> dict:
1462        """
1463        Requesting actual user's portfolio for current `accountId`.
1464
1465        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1466
1467        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1468
1469        :return: dictionary with user's portfolio.
1470        """
1471        if self.accountId is None or not self.accountId:
1472            uLogger.error("Variable `accountId` must be defined for using this method!")
1473            raise Exception("Account ID required")
1474
1475        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1476
1477        self.body = str({"accountId": self.accountId})
1478        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1479        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1480
1481        if self.moreDebug:
1482            uLogger.debug("Records about user's portfolio successfully received")
1483
1484        return rawPortfolio

Requesting actual user's portfolio for current accountId.

REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio

Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1486    def RequestPositions(self) -> dict:
1487        """
1488        Requesting open positions by currencies and instruments for current `accountId`.
1489
1490        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1491
1492        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1493
1494        :return: dictionary with open positions by instruments.
1495        """
1496        if self.accountId is None or not self.accountId:
1497            uLogger.error("Variable `accountId` must be defined for using this method!")
1498            raise Exception("Account ID required")
1499
1500        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1501
1502        self.body = str({"accountId": self.accountId})
1503        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1504        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1505
1506        if self.moreDebug:
1507            uLogger.debug("Records about current open positions successfully received")
1508
1509        return rawPositions

Requesting open positions by currencies and instruments for current accountId.

REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions

Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1511    def RequestPendingOrders(self) -> list:
1512        """
1513        Requesting current actual pending limit orders for current `accountId`.
1514
1515        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1516
1517        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1518
1519        :return: list of dictionaries with pending limit orders.
1520        """
1521        if self.accountId is None or not self.accountId:
1522            uLogger.error("Variable `accountId` must be defined for using this method!")
1523            raise Exception("Account ID required")
1524
1525        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1526
1527        self.body = str({"accountId": self.accountId})
1528        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1529        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1530
1531        uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1532
1533        return rawOrders

Requesting current actual pending limit orders for current accountId.

REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders

Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending limit orders.

def RequestStopOrders(self) -> list:
1535    def RequestStopOrders(self) -> list:
1536        """
1537        Requesting current actual stop orders for current `accountId`.
1538
1539        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1540
1541        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1542
1543        :return: list of dictionaries with stop orders.
1544        """
1545        if self.accountId is None or not self.accountId:
1546            uLogger.error("Variable `accountId` must be defined for using this method!")
1547            raise Exception("Account ID required")
1548
1549        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1550
1551        self.body = str({"accountId": self.accountId})
1552        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1553        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1554
1555        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1556
1557        return rawStopOrders

Requesting current actual stop orders for current accountId.

REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders

Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full') -> dict:
1559    def Overview(self, show: bool = False, details: str = "full") -> dict:
1560        """
1561        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1562        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1563        and `overviewBondsCalendarFile` are defined then also save information to file.
1564
1565        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1566        many requests about the state of the portfolio, and then, based on the received data, a large number
1567        of calculation and statistics are collected.
1568
1569        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1570        :param details: how detailed should the information be?
1571        - `full` — shows full available information about portfolio status (by default),
1572        - `positions` — shows only open positions,
1573        - `orders` — shows only sections of open limits and stop orders.
1574        - `digest` — show a short digest of the portfolio status,
1575        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1576        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1577        :return: dictionary with client's raw portfolio and some statistics.
1578        """
1579        if self.accountId is None or not self.accountId:
1580            uLogger.error("Variable `accountId` must be defined for using this method!")
1581            raise Exception("Account ID required")
1582
1583        view = {
1584            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1585                "headers": {},  # list of dictionaries, response headers without "positions" section
1586                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1587                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1588                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1589                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1590                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1591                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1592                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1593                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1594                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1595            },
1596            "stat": {  # --- some statistics calculated using "raw" sections:
1597                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1598                "availableRUB": 0.,  # available rubles (without other currencies)
1599                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1600                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1601                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1602                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1603                "sharesCostRUB": 0.,  # costs of all shares in RUB
1604                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1605                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1606                "futuresCostRUB": 0.,  # costs of all futures in RUB
1607                "Currencies": [],  # list of dictionaries of all currencies statistics
1608                "Shares": [],  # list of dictionaries of all shares statistics
1609                "Bonds": [],  # list of dictionaries of all bonds statistics
1610                "Etfs": [],  # list of dictionaries of all etfs statistics
1611                "Futures": [],  # list of dictionaries of all futures statistics
1612                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1613                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1614                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1615                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1616                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1617            },
1618            "analytics": {  # --- some analytics of portfolio:
1619                "distrByAssets": {},  # portfolio distribution by assets
1620                "distrByCompanies": {},  # portfolio distribution by companies
1621                "distrBySectors": {},  # portfolio distribution by sectors
1622                "distrByCurrencies": {},  # portfolio distribution by currencies
1623                "distrByCountries": {},  # portfolio distribution by countries
1624                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1625            }
1626        }
1627
1628        details = details.lower()
1629        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1630        if details not in availableDetails:
1631            details = "full"
1632            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1633
1634        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1635
1636        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1637        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1638        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1639        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1640
1641        # save response headers without "positions" section:
1642        for key in portfolioResponse.keys():
1643            if key != "positions":
1644                view["raw"]["headers"][key] = portfolioResponse[key]
1645
1646            else:
1647                continue
1648
1649        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1650        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1651        for item in portfolioResponse["positions"]:
1652            if item["instrumentType"] == "currency":
1653                self._figi = item["figi"]
1654                curr = self.SearchByFIGI(requestPrice=False)
1655
1656                # current price of currency in RUB:
1657                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1658                    "name": curr["name"],
1659                    "currentPrice": NanoToFloat(
1660                        item["currentPrice"]["units"],
1661                        item["currentPrice"]["nano"]
1662                    ),
1663                }
1664
1665                view["raw"]["Currencies"].append(item)
1666
1667            elif item["instrumentType"] == "share":
1668                view["raw"]["Shares"].append(item)
1669
1670            elif item["instrumentType"] == "bond":
1671                view["raw"]["Bonds"].append(item)
1672
1673            elif item["instrumentType"] == "etf":
1674                view["raw"]["Etfs"].append(item)
1675
1676            elif item["instrumentType"] == "futures":
1677                view["raw"]["Futures"].append(item)
1678
1679            else:
1680                continue
1681
1682        # how many volume of currencies (by ISO currency name) are blocked:
1683        for item in view["raw"]["positions"]["blocked"]:
1684            blocked = NanoToFloat(item["units"], item["nano"])
1685            if blocked > 0:
1686                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1687
1688        # how many volume of instruments (by FIGI) are blocked:
1689        for item in view["raw"]["positions"]["securities"]:
1690            blocked = int(item["blocked"])
1691            if blocked > 0:
1692                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1693
1694        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1695
1696        if "rub" in allBlocked.keys():
1697            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1698
1699        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1700        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1701        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1702        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1703        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1704        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1705        view["stat"]["portfolioCostRUB"] = sum([
1706            view["stat"]["allCurrenciesCostRUB"],
1707            view["stat"]["sharesCostRUB"],
1708            view["stat"]["bondsCostRUB"],
1709            view["stat"]["etfsCostRUB"],
1710            view["stat"]["futuresCostRUB"],
1711        ])
1712
1713        # --- calculating some portfolio statistics:
1714        byComp = {}  # distribution by companies
1715        bySect = {}  # distribution by sectors
1716        byCurr = {}  # distribution by currencies (include RUB)
1717        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1718        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1719
1720        for item in portfolioResponse["positions"]:
1721            self._figi = item["figi"]
1722            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1723
1724            if instrument:
1725                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1726                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1727
1728                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1729                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1730
1731                else:
1732                    blocked = 0
1733
1734                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1735                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1736                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1737                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1738                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1739                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1740                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1741                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1742                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1743                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1744                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1745                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1746
1747                statData = {
1748                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1749                    "ticker": instrument["ticker"],  # ticker by FIGI
1750                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1751                    "volume": volume,  # available volume of instrument
1752                    "lots": lots,  # volume in lots of instrument
1753                    "direction": direction,  # direction of an instrument's position: short or long
1754                    "blocked": blocked,  # blocked volume of currency or instrument
1755                    "currentPrice": curPrice,  # current instrument's price in basic asset
1756                    "average": average,  # current average position price
1757                    "cost": cost,  # current cost of all volume of instrument in basic asset
1758                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1759                    "costRUB": costRUB,  # cost of instrument in ruble
1760                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1761                    "profit": profit,  # expected profit at current moment
1762                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1763                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1764                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1765                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1766                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1767                    "step": instrument["step"],  # minimum price increment
1768                }
1769
1770                # adding distribution by unique countries:
1771                if statData["country"] not in byCountry.keys():
1772                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1773
1774                else:
1775                    byCountry[statData["country"]]["cost"] += costRUB
1776                    byCountry[statData["country"]]["percent"] += percentCostRUB
1777
1778                if item["instrumentType"] != "currency":
1779                    # adding distribution by unique companies:
1780                    if statData["name"]:
1781                        if statData["name"] not in byComp.keys():
1782                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1783
1784                        else:
1785                            byComp[statData["name"]]["cost"] += costRUB
1786                            byComp[statData["name"]]["percent"] += percentCostRUB
1787
1788                    # adding distribution by unique sectors:
1789                    if statData["sector"] not in bySect.keys():
1790                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1791
1792                    else:
1793                        bySect[statData["sector"]]["cost"] += costRUB
1794                        bySect[statData["sector"]]["percent"] += percentCostRUB
1795
1796                # adding distribution by unique currencies:
1797                if currency not in byCurr.keys():
1798                    byCurr[currency] = {
1799                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1800                        "cost": costRUB,
1801                        "percent": percentCostRUB
1802                    }
1803
1804                else:
1805                    byCurr[currency]["cost"] += costRUB
1806                    byCurr[currency]["percent"] += percentCostRUB
1807
1808                # saving statistics for every instrument:
1809                if item["instrumentType"] == "currency":
1810                    view["stat"]["Currencies"].append(statData)
1811
1812                    # update dict with free funds for trading (total - blocked) by currencies
1813                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1814                    view["stat"]["funds"][currency] = {
1815                        "total": volume,
1816                        "totalCostRUB": costRUB,  # total volume cost in rubles
1817                        "free": volume - blocked,
1818                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1819                    }
1820
1821                elif item["instrumentType"] == "share":
1822                    view["stat"]["Shares"].append(statData)
1823
1824                elif item["instrumentType"] == "bond":
1825                    view["stat"]["Bonds"].append(statData)
1826
1827                elif item["instrumentType"] == "etf":
1828                    view["stat"]["Etfs"].append(statData)
1829
1830                elif item["instrumentType"] == "Futures":
1831                    view["stat"]["Futures"].append(statData)
1832
1833                else:
1834                    continue
1835
1836        # total changes in Russian Ruble:
1837        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1838        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1839        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1840        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1841        view["stat"]["funds"]["rub"] = {
1842            "total": view["stat"]["availableRUB"],
1843            "totalCostRUB": view["stat"]["availableRUB"],
1844            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1845            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1846        }
1847
1848        # --- pending limit orders sector data:
1849        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1850        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1851
1852        for item in view["raw"]["orders"]:
1853            self._figi = item["figi"]
1854
1855            if item["figi"] not in uniquePendingOrdersFIGIs:
1856                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1857
1858                uniquePendingOrdersFIGIs.append(item["figi"])
1859                uniquePendingOrders[item["figi"]] = instrument
1860
1861            else:
1862                instrument = uniquePendingOrders[item["figi"]]
1863
1864            if instrument:
1865                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1866                orderType = TKS_ORDER_TYPES[item["orderType"]]
1867                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1868                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1869
1870                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1871                if item["direction"] == "ORDER_DIRECTION_BUY":
1872                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1873
1874                else:
1875                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1876
1877                # requested price for order execution:
1878                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1879
1880                # necessary changes in percent to reach target from current price:
1881                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1882
1883                view["stat"]["orders"].append({
1884                    "orderID": item["orderId"],  # orderId number parameter of current order
1885                    "figi": item["figi"],  # FIGI identification
1886                    "ticker": instrument["ticker"],  # ticker name by FIGI
1887                    "lotsRequested": item["lotsRequested"],  # requested lots value
1888                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1889                    "currentPrice": lastPrice,  # current instrument's price for defined action
1890                    "targetPrice": target,  # requested price for order execution in base currency
1891                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1892                    "percentChanges": changes,  # changes in percent to target from current price
1893                    "currency": item["currency"],  # instrument's currency name
1894                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1895                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1896                    "status": orderState,  # order status from TKS_ORDER_STATES
1897                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1898                })
1899
1900        # --- stop orders sector data:
1901        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1902        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1903
1904        for item in view["raw"]["stopOrders"]:
1905            self._figi = item["figi"]
1906
1907            if item["figi"] not in uniqueStopOrdersFIGIs:
1908                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1909
1910                uniqueStopOrdersFIGIs.append(item["figi"])
1911                uniqueStopOrders[item["figi"]] = instrument
1912
1913            else:
1914                instrument = uniqueStopOrders[item["figi"]]
1915
1916            if instrument:
1917                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1918                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1919                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1920
1921                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1922                if "expirationTime" in item.keys():
1923                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1924                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1925
1926                else:
1927                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1928                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1929
1930                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1931                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1932                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1933
1934                else:
1935                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1936
1937                # requested price when stop-order executed:
1938                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1939
1940                # price for limit-order, set up when stop-order executed:
1941                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1942
1943                # necessary changes in percent to reach target from current price:
1944                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1945
1946                view["stat"]["stopOrders"].append({
1947                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1948                    "figi": item["figi"],  # FIGI identification
1949                    "ticker": instrument["ticker"],  # ticker name by FIGI
1950                    "lotsRequested": item["lotsRequested"],  # requested lots value
1951                    "currentPrice": lastPrice,  # current instrument's price for defined action
1952                    "targetPrice": target,  # requested price for stop-order execution in base currency
1953                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1954                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1955                    "percentChanges": changes,  # changes in percent to target from current price
1956                    "currency": item["currency"],  # instrument's currency name
1957                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1958                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1959                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1960                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1961                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1962                })
1963
1964        # --- calculating data for analytics section:
1965        # portfolio distribution by assets:
1966        view["analytics"]["distrByAssets"] = {
1967            "Ruble": {
1968                "uniques": 1,
1969                "cost": view["stat"]["availableRUB"],
1970                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1971            },
1972            "Currencies": {
1973                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1974                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1975                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1976            },
1977            "Shares": {
1978                "uniques": len(view["stat"]["Shares"]),
1979                "cost": view["stat"]["sharesCostRUB"],
1980                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1981            },
1982            "Bonds": {
1983                "uniques": len(view["stat"]["Bonds"]),
1984                "cost": view["stat"]["bondsCostRUB"],
1985                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1986            },
1987            "Etfs": {
1988                "uniques": len(view["stat"]["Etfs"]),
1989                "cost": view["stat"]["etfsCostRUB"],
1990                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1991            },
1992            "Futures": {
1993                "uniques": len(view["stat"]["Futures"]),
1994                "cost": view["stat"]["futuresCostRUB"],
1995                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1996            },
1997        }
1998
1999        # portfolio distribution by companies:
2000        view["analytics"]["distrByCompanies"]["All money cash"] = {
2001            "ticker": "",
2002            "cost": view["stat"]["allCurrenciesCostRUB"],
2003            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2004        }
2005        view["analytics"]["distrByCompanies"].update(byComp)
2006
2007        # portfolio distribution by sectors:
2008        view["analytics"]["distrBySectors"]["All money cash"] = {
2009            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2010            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2011        }
2012        view["analytics"]["distrBySectors"].update(bySect)
2013
2014        # portfolio distribution by currencies:
2015        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2016            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2017
2018            if self.moreDebug:
2019                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2020
2021        view["analytics"]["distrByCurrencies"].update(byCurr)
2022        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2023        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2024
2025        # portfolio distribution by countries:
2026        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2027            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2028
2029            if self.moreDebug:
2030                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2031
2032        view["analytics"]["distrByCountries"].update(byCountry)
2033        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2034        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2035
2036        # --- Prepare text statistics overview in human-readable:
2037        if show:
2038            # Whatever the value `details`, header not changes:
2039            info = [
2040                "# Client's portfolio\n\n",
2041                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2042                "* **Account ID:** [{}]\n".format(self.accountId),
2043            ]
2044
2045            if details in ["full", "positions", "digest"]:
2046                info.extend([
2047                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2048                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2049                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2050                        view["stat"]["totalChangesRUB"],
2051                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2052                        view["stat"]["totalChangesPercentRUB"],
2053                    ),
2054                ])
2055
2056            if details in ["full", "positions"]:
2057                info.extend([
2058                    "## Open positions\n\n",
2059                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2060                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2061                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2062                        "{:.2f} ({:.2f}) rub".format(
2063                            view["stat"]["availableRUB"],
2064                            view["stat"]["blockedRUB"],
2065                        )
2066                    )
2067                ])
2068
2069                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2070                    return [
2071                        "|                             |                                 |          |              |              |                     |                              |\n",
2072                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2073                            noTradeStr if noTradeStr else typeStr,
2074                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2075                        ),
2076                    ]
2077
2078                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2079                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2080                        "{} [{}]".format(data["ticker"], data["figi"]),
2081                        "{:.2f} ({:.2f}) {}".format(
2082                            data["volume"],
2083                            data["blocked"],
2084                            data["currency"],
2085                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2086                            data["volume"],
2087                            data["blocked"],
2088                        ),
2089                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2090                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2091                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2092                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2093                        "{}{:.2f} {} ({}{:.2f}%)".format(
2094                            "+" if data["profit"] > 0 else "",
2095                            data["profit"], data["baseCurrencyName"],
2096                            "+" if data["percentProfit"] > 0 else "",
2097                            data["percentProfit"],
2098                        ),
2099                    )
2100
2101                # --- Show currencies section:
2102                if view["stat"]["Currencies"]:
2103                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2104                    for item in view["stat"]["Currencies"]:
2105                        info.append(_InfoStr(item, showCurrencyName=True))
2106
2107                else:
2108                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2109
2110                # --- Show shares section:
2111                if view["stat"]["Shares"]:
2112                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2113
2114                    for item in view["stat"]["Shares"]:
2115                        info.append(_InfoStr(item))
2116
2117                else:
2118                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2119
2120                # --- Show bonds section:
2121                if view["stat"]["Bonds"]:
2122                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2123
2124                    for item in view["stat"]["Bonds"]:
2125                        info.append(_InfoStr(item))
2126
2127                else:
2128                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2129
2130                # --- Show etfs section:
2131                if view["stat"]["Etfs"]:
2132                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2133
2134                    for item in view["stat"]["Etfs"]:
2135                        info.append(_InfoStr(item))
2136
2137                else:
2138                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2139
2140                # --- Show futures section:
2141                if view["stat"]["Futures"]:
2142                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2143
2144                    for item in view["stat"]["Futures"]:
2145                        info.append(_InfoStr(item))
2146
2147                else:
2148                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2149
2150            if details in ["full", "orders"]:
2151                # --- Show pending limit orders section:
2152                if view["stat"]["orders"]:
2153                    info.extend([
2154                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2155                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2156                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2157                    ])
2158
2159                    for item in view["stat"]["orders"]:
2160                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2161                            "{} [{}]".format(item["ticker"], item["figi"]),
2162                            item["orderID"],
2163                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2164                            "{} {} ({}{:.2f}%)".format(
2165                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2166                                item["baseCurrencyName"],
2167                                "+" if item["percentChanges"] > 0 else "",
2168                                float(item["percentChanges"]),
2169                            ),
2170                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2171                            item["action"],
2172                            item["type"],
2173                            item["date"],
2174                        ))
2175
2176                else:
2177                    info.append("\n## Total pending limit-orders: 0\n")
2178
2179                # --- Show stop orders section:
2180                if view["stat"]["stopOrders"]:
2181                    info.extend([
2182                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2183                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2184                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2185                    ])
2186
2187                    for item in view["stat"]["stopOrders"]:
2188                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2189                            "{} [{}]".format(item["ticker"], item["figi"]),
2190                            item["orderID"],
2191                            item["lotsRequested"],
2192                            "{} {} ({}{:.2f}%)".format(
2193                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2194                                item["baseCurrencyName"],
2195                                "+" if item["percentChanges"] > 0 else "",
2196                                float(item["percentChanges"]),
2197                            ),
2198                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2199                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2200                            item["action"],
2201                            item["type"],
2202                            item["expType"],
2203                            item["createDate"],
2204                            item["expDate"],
2205                        ))
2206
2207                else:
2208                    info.append("\n## Total stop-orders: 0\n")
2209
2210            if details in ["full", "analytics"]:
2211                # -- Show analytics section:
2212                if view["stat"]["portfolioCostRUB"] > 0:
2213                    info.extend([
2214                        "\n# Analytics\n"
2215                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2216                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2217                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2218                            view["stat"]["totalChangesRUB"],
2219                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2220                            view["stat"]["totalChangesPercentRUB"],
2221                        ),
2222                        "\n## Portfolio distribution by assets\n"
2223                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2224                        "|------------------------------------|---------|---------|--------------------|\n",
2225                    ])
2226
2227                    for key in view["analytics"]["distrByAssets"].keys():
2228                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2229                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2230                                key,
2231                                view["analytics"]["distrByAssets"][key]["uniques"],
2232                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2233                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2234                            ))
2235
2236                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2237
2238                    info.extend([
2239                        "\n## Portfolio distribution by companies\n"
2240                        "\n| Company                                      | Percent | Current cost       |\n",
2241                        aSepLine,
2242                    ])
2243
2244                    for company in view["analytics"]["distrByCompanies"].keys():
2245                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2246                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2247                                "{}{}".format(
2248                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2249                                    company,
2250                                ),
2251                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2252                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2253                            ))
2254
2255                    info.extend([
2256                        "\n## Portfolio distribution by sectors\n"
2257                        "\n| Sector                                       | Percent | Current cost       |\n",
2258                        aSepLine,
2259                    ])
2260
2261                    for sector in view["analytics"]["distrBySectors"].keys():
2262                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2263                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2264                                sector,
2265                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2266                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2267                            ))
2268
2269                    info.extend([
2270                        "\n## Portfolio distribution by currencies\n"
2271                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2272                        aSepLine,
2273                    ])
2274
2275                    for curr in view["analytics"]["distrByCurrencies"].keys():
2276                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2277                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2278                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2279                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2280                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2281                            ))
2282
2283                    info.extend([
2284                        "\n## Portfolio distribution by countries\n"
2285                        "\n| Assets by country                            | Percent | Current cost       |\n",
2286                        aSepLine,
2287                    ])
2288
2289                    for country in view["analytics"]["distrByCountries"].keys():
2290                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2291                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2292                                country,
2293                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2294                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2295                            ))
2296
2297            if details in ["full", "calendar"]:
2298                # -- Show bonds payment calendar section:
2299                if view["stat"]["Bonds"]:
2300                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2301                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2302                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2303
2304                else:
2305                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2306
2307            infoText = "".join(info)
2308
2309            uLogger.info(infoText)
2310
2311            if details == "full" and self.overviewFile:
2312                filename = self.overviewFile
2313
2314            elif details == "digest" and self.overviewDigestFile:
2315                filename = self.overviewDigestFile
2316
2317            elif details == "positions" and self.overviewPositionsFile:
2318                filename = self.overviewPositionsFile
2319
2320            elif details == "orders" and self.overviewOrdersFile:
2321                filename = self.overviewOrdersFile
2322
2323            elif details == "analytics" and self.overviewAnalyticsFile:
2324                filename = self.overviewAnalyticsFile
2325
2326            elif details == "calendar" and self.overviewBondsCalendarFile:
2327                filename = self.overviewBondsCalendarFile
2328
2329            else:
2330                filename = ""
2331
2332            if filename:
2333                with open(filename, "w", encoding="UTF-8") as fH:
2334                    fH.write(infoText)
2335
2336                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2337
2338        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile and overviewBondsCalendarFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be?
    • full — shows full available information about portfolio status (by default),
    • positions — shows only open positions,
    • orders — shows only sections of open limits and stop orders.
    • digest — show a short digest of the portfolio status,
    • analytics — shows only the analytics section and the distribution of the portfolio by various categories,
    • calendar — shows only the bonds calendar section (if these present in portfolio),
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2340    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2341        """
2342        Returns history operations between two given dates for current `accountId`.
2343        If `reportFile` string is not empty then also save human-readable report.
2344        Shows some statistical data of closed positions.
2345
2346        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2347        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2348        :param show: if `True` then also prints all records to the console.
2349        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2350        :return: original list of dictionaries with history of deals records from API ("operations" key):
2351                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2352                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2353        """
2354        if self.accountId is None or not self.accountId:
2355            uLogger.error("Variable `accountId` must be defined for using this method!")
2356            raise Exception("Account ID required")
2357
2358        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2359
2360        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2361
2362        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2363        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2364        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2365        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2366        customStat = {}  # custom statistics in additional to responseJSON
2367
2368        # --- output report in human-readable format:
2369        if show or self.reportFile:
2370            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2371            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2372            nextDay = ""
2373
2374            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2375
2376            if len(ops) > 0:
2377                customStat = {
2378                    "opsCount": 0,  # total operations count
2379                    "buyCount": 0,  # buy operations
2380                    "sellCount": 0,  # sell operations
2381                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2382                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2383                    "payIn": {"rub": 0.},  # Deposit brokerage account
2384                    "payOut": {"rub": 0.},  # Withdrawals
2385                    "divs": {"rub": 0.},  # Dividends income
2386                    "coupons": {"rub": 0.},  # Coupon's income
2387                    "brokerCom": {"rub": 0.},  # Service commissions
2388                    "serviceCom": {"rub": 0.},  # Service commissions
2389                    "marginCom": {"rub": 0.},  # Margin commissions
2390                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2391                }
2392
2393                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2394                for item in ops:
2395                    if item["state"] == "OPERATION_STATE_EXECUTED":
2396                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2397
2398                        # count buy operations:
2399                        if "_BUY" in item["operationType"]:
2400                            customStat["buyCount"] += 1
2401
2402                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2403                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2404
2405                            else:
2406                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2407
2408                        # count sell operations:
2409                        elif "_SELL" in item["operationType"]:
2410                            customStat["sellCount"] += 1
2411
2412                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2413                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2414
2415                            else:
2416                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2417
2418                        # count incoming operations:
2419                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2420                            if item["payment"]["currency"] in customStat["payIn"].keys():
2421                                customStat["payIn"][item["payment"]["currency"]] += payment
2422
2423                            else:
2424                                customStat["payIn"][item["payment"]["currency"]] = payment
2425
2426                        # count withdrawals operations:
2427                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2428                            if item["payment"]["currency"] in customStat["payOut"].keys():
2429                                customStat["payOut"][item["payment"]["currency"]] += payment
2430
2431                            else:
2432                                customStat["payOut"][item["payment"]["currency"]] = payment
2433
2434                        # count dividends income:
2435                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2436                            if item["payment"]["currency"] in customStat["divs"].keys():
2437                                customStat["divs"][item["payment"]["currency"]] += payment
2438
2439                            else:
2440                                customStat["divs"][item["payment"]["currency"]] = payment
2441
2442                        # count coupon's income:
2443                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2444                            if item["payment"]["currency"] in customStat["coupons"].keys():
2445                                customStat["coupons"][item["payment"]["currency"]] += payment
2446
2447                            else:
2448                                customStat["coupons"][item["payment"]["currency"]] = payment
2449
2450                        # count broker commissions:
2451                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2452                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2453                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2454
2455                            else:
2456                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2457
2458                        # count service commissions:
2459                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2460                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2461                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2462
2463                            else:
2464                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2465
2466                        # count margin commissions:
2467                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2468                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2469                                customStat["marginCom"][item["payment"]["currency"]] += payment
2470
2471                            else:
2472                                customStat["marginCom"][item["payment"]["currency"]] = payment
2473
2474                        # count withholding taxes:
2475                        elif "_TAX" in item["operationType"]:
2476                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2477                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2478
2479                            else:
2480                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2481
2482                        else:
2483                            continue
2484
2485                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2486
2487                # --- view "Actions" lines:
2488                info.extend([
2489                    "| Report sections            |                               |                              |                      |                        |\n",
2490                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2491                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2492                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2493                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2494                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2495                    ),
2496                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2497                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2498                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2499                    ),
2500                ])
2501
2502                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2503                for key in opsKeys:
2504                    if key == "rub":
2505                        continue
2506
2507                    info.extend([
2508                        "|                            |                               | {:<28} |                      |                        |\n".format(
2509                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2510                        ),
2511                        "|                            |                               | {:<28} |                      |                        |\n".format(
2512                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2513                        ),
2514                    ])
2515
2516                info.append(splitLine1)
2517
2518                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2519                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2520                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2521                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2522                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2523                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2524                    )
2525
2526                # --- view "Payments" lines:
2527                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2528                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2529
2530                for key in paymentsKeys:
2531                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2532
2533                info.append(splitLine1)
2534
2535                # --- view "Commissions and taxes" lines:
2536                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2537                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2538
2539                for key in comKeys:
2540                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2541
2542                info.append(splitLine1)
2543
2544                info.extend([
2545                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2546                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2547                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2548                ])
2549
2550            else:
2551                info.append("Broker returned no operations during this period\n")
2552
2553            # --- view "Operations" section:
2554            for item in ops:
2555                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2556                    continue
2557
2558                else:
2559                    self._figi = item["figi"] if item["figi"] else ""
2560                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2561                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2562
2563                    # group of deals during one day:
2564                    if nextDay and item["date"].split("T")[0] != nextDay:
2565                        info.append(splitLine2)
2566                        nextDay = ""
2567
2568                    else:
2569                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2570
2571                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2572                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2573                        self._figi if self._figi else "—",
2574                        instrument["ticker"] if instrument else "—",
2575                        instrument["type"] if instrument else "—",
2576                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2577                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2578                        TKS_OPERATION_STATES[item["state"]],
2579                        TKS_OPERATION_TYPES[item["operationType"]],
2580                    ))
2581
2582            infoText = "".join(info)
2583
2584            if show:
2585                if self.moreDebug:
2586                    uLogger.debug("Records about history of a client's operations successfully received")
2587
2588                uLogger.info(infoText)
2589
2590            if self.reportFile:
2591                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2592                    fH.write(infoText)
2593
2594                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2595
2596        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False) -> pandas.core.frame.DataFrame:
2598    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2599        """
2600        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2601
2602        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2603        Warning! Broker server used ISO UTC time by default.
2604
2605        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2606        Also, `historyFile` used to update history with `onlyMissing` parameter.
2607
2608        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2609
2610        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2611        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2612        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2613                         `"hour"`, `"day"`. Default: `"hour"`.
2614        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2615                            False by default. Warning! History appends only from last candle to current time
2616                            with always update last candle!
2617        :param csvSep: separator if csv-file is used, `,` by default.
2618        :param show: if `True` then also prints Pandas DataFrame to the console.
2619        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2620                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2621        """
2622        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2623        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2624        history = None  # empty pandas object for history
2625
2626        if interval not in TKS_CANDLE_INTERVALS.keys():
2627            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2628            raise Exception("Incorrect value")
2629
2630        if not (self._ticker or self._figi):
2631            uLogger.error("Ticker or FIGI must be defined!")
2632            raise Exception("Ticker or FIGI required")
2633
2634        if self._ticker and not self._figi:
2635            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2636            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2637
2638        if self._figi and not self._ticker:
2639            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2640            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2641
2642        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2643        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2644        if interval.lower() != "day":
2645            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2646
2647        delta = dtEnd - dtStart  # current UTC time minus last time in file
2648        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2649
2650        # calculate history length in candles:
2651        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2652        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2653            length += 1  # to avoid fraction time
2654
2655        # calculate data blocks count:
2656        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2657
2658        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2659        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2660        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2661        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2662        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2663
2664        tempOld = None  # pandas object for old history, if --only-missing key present
2665        lastTime = None  # datetime object of last old candle in file
2666
2667        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2668            uLogger.debug("--only-missing key present, add only last missing candles...")
2669            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2670
2671            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2672
2673            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2674            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2675            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2676            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2677
2678            # get last datetime object from last string in file or minus 1 delta if file is empty:
2679            if len(tempOld) > 0:
2680                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2681
2682            else:
2683                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2684
2685            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2686
2687        responseJSONs = []  # raw history blocks of data
2688
2689        blockEnd = dtEnd
2690        for item in range(blocks):
2691            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2692            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2693
2694            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2695                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2696            ))
2697
2698            if blockStart == blockEnd:
2699                uLogger.debug("Skipped this zero-length block...")
2700
2701            else:
2702                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2703                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2704                self.body = str({
2705                    "figi": self._figi,
2706                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2707                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2708                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2709                })
2710                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2711
2712                if "code" in responseJSON.keys():
2713                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2714
2715                else:
2716                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2717                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2718
2719                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2720
2721            blockEnd = blockStart
2722
2723        printCount = len(responseJSONs)  # candles to show in console
2724        if responseJSONs:
2725            tempHistory = pd.DataFrame(
2726                data={
2727                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2728                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2729                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2730                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2731                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2732                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2733                    "volume": [int(item["volume"]) for item in responseJSONs],
2734                },
2735                index=range(len(responseJSONs)),
2736                columns=["date", "time", "open", "high", "low", "close", "volume"],
2737            )
2738            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2739            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2740
2741            # append only newest candles to old history if --only-missing key present:
2742            if onlyMissing and tempOld is not None and lastTime is not None:
2743                index = 0  # find start index in tempHistory data:
2744
2745                for i, item in tempHistory.iterrows():
2746                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2747
2748                    if curTime == lastTime:
2749                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2750                        index = i
2751                        printCount = index + 1
2752                        break
2753
2754                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2755
2756            else:
2757                history = tempHistory  # if no `--only-missing` key then load full data from server
2758
2759            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2760
2761        if history is not None and not history.empty:
2762            if show:
2763                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2764                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2765                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2766                ))
2767
2768        else:
2769            uLogger.warning("Received an empty candles history!")
2770
2771        if self.historyFile is not None:
2772            if history is not None and not history.empty:
2773                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2774                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2775
2776            else:
2777                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2778
2779        else:
2780            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2781
2782        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2784    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2785        """
2786        Load candles history from csv-file and return Pandas DataFrame object.
2787
2788        See also: `History()` and `ShowHistoryChart()` methods.
2789
2790        :param filePath: path to csv-file to open.
2791        """
2792        loadedHistory = None  # init candles data object
2793
2794        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2795
2796        if os.path.exists(filePath):
2797            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2798
2799            tfStr = self.priceModel.FormattedDelta(
2800                self.priceModel.timeframe,
2801                "{days} days {hours}h {minutes}m {seconds}s",
2802            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2803                self.priceModel.timeframe,
2804                "{hours}h {minutes}m {seconds}s",
2805            )
2806
2807            if loadedHistory is not None and not loadedHistory.empty:
2808                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2809                    len(loadedHistory),
2810                    tfStr,
2811                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2812                )
2813
2814            else:
2815                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2816
2817        else:
2818            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2819
2820        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2822    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2823        """
2824        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2825
2826        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2827        Default: `index.html` (both for interact and non-interact candlesticks chart).
2828
2829        See also: `History()` and `LoadHistory()` methods.
2830
2831        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2832        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2833                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2834                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2835                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2836        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2837                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2838        """
2839        if isinstance(candles, str):
2840            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2841            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2842
2843        elif isinstance(candles, pd.DataFrame):
2844            self.priceModel.prices = candles  # set candles chain from variable
2845            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2846
2847            if "datetime" not in candles.columns:
2848                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2849
2850        else:
2851            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2852            raise Exception("Incorrect value")
2853
2854        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2855
2856        if interact:
2857            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2858
2859            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2860
2861        else:
2862            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2863
2864            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2865
2866        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2868    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2869        """
2870        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2871        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2872
2873        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2874
2875        :param operation: string "Buy" or "Sell".
2876        :param lots: volume, integer count of lots >= 1.
2877        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2878        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2879        :param expDate: string "Undefined" by default or local date in future,
2880                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2881        :return: JSON with response from broker server.
2882        """
2883        if self.accountId is None or not self.accountId:
2884            uLogger.error("Variable `accountId` must be defined for using this method!")
2885            raise Exception("Account ID required")
2886
2887        if operation is None or not operation or operation not in ("Buy", "Sell"):
2888            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2889            raise Exception("Incorrect value")
2890
2891        if lots is None or lots < 1:
2892            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2893            lots = 1
2894
2895        if tp is None or tp < 0:
2896            tp = 0
2897
2898        if sl is None or sl < 0:
2899            sl = 0
2900
2901        if expDate is None or not expDate:
2902            expDate = "Undefined"
2903
2904        if not (self._ticker or self._figi):
2905            uLogger.error("Ticker or FIGI must be defined!")
2906            raise Exception("Ticker or FIGI required")
2907
2908        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2909        self._ticker = instrument["ticker"]
2910        self._figi = instrument["figi"]
2911
2912        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
2913
2914        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2915        self.body = str({
2916            "figi": self._figi,
2917            "quantity": str(lots),
2918            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2919            "accountId": str(self.accountId),
2920            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2921        })
2922        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2923
2924        if "orderId" in response.keys():
2925            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2926                operation, response["orderId"],
2927                self._ticker, self._figi, lots,
2928                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2929                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2930                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2931            ))
2932
2933            if tp > 0:
2934                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2935
2936            if sl > 0:
2937                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2938
2939        else:
2940            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
2941
2942        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2944    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2945        """
2946        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2947        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2948
2949        See also: `Order()` and `Trade()` docstrings.
2950
2951        :param lots: volume, integer count of lots >= 1.
2952        :param tp: float > 0, take profit price of stop-order.
2953        :param sl: float > 0, stop loss price of stop-order.
2954        :param expDate: it's a local date in future.
2955                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2956        :return: JSON with response from broker server.
2957        """
2958        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2960    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2961        """
2962        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2963        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2964
2965        See also: `Order()` and `Trade()` docstrings.
2966
2967        :param lots: volume, integer count of lots >= 1.
2968        :param tp: float > 0, take profit price of stop-order.
2969        :param sl: float > 0, stop loss price of stop-order.
2970        :param expDate: it's a local date in the future.
2971                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2972        :return: JSON with response from broker server.
2973        """
2974        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
2976    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
2977        """
2978        Close position of given instruments.
2979
2980        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
2981        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2982                         This avoids unnecessary downloading data from the server.
2983        """
2984        if instruments is None or not instruments:
2985            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
2986            raise Exception("Ticker or FIGI required")
2987
2988        if isinstance(instruments, str):
2989            instruments = [instruments]
2990
2991        uniqueInstruments = self.GetUniqueFIGIs(instruments)
2992        if uniqueInstruments:
2993            if portfolio is None or not portfolio:
2994                portfolio = self.Overview(show=False)
2995
2996            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
2997            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
2998
2999            for self._figi in uniqueInstruments:
3000                if self._figi not in allOpened:
3001                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3002                    continue
3003
3004                # search open trade info about instrument by ticker:
3005                instrument = {}
3006                for iType in TKS_INSTRUMENTS:
3007                    if instrument:
3008                        break
3009
3010                    for item in portfolio["stat"][iType]:
3011                        if item["figi"] == self._figi:
3012                            instrument = item
3013                            break
3014
3015                if instrument:
3016                    self._ticker = instrument["ticker"]
3017                    self._figi = instrument["figi"]
3018
3019                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3020                        self._ticker,
3021                        self._figi,
3022                        int(instrument["volume"]),
3023                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3024                    ))
3025
3026                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3027
3028                    if tradeLots > 0:
3029                        if instrument["blocked"] > 0:
3030                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3031                                instrument["blocked"],
3032                                self._ticker,
3033                                tradeLots,
3034                            ))
3035
3036                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3037                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3038
3039                    else:
3040                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))

Close position of given instruments.

Parameters
  • instruments: list of instruments defined by tickers or FIGIs that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3042    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3043        """
3044        Close all positions of given instruments with defined type.
3045
3046        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3047        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3048                         This avoids unnecessary downloading data from the server.
3049        """
3050        if iType not in TKS_INSTRUMENTS:
3051            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3052
3053        else:
3054            if portfolio is None or not portfolio:
3055                portfolio = self.Overview(show=False)
3056
3057            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3058            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3059
3060            if tickers and portfolio:
3061                self.CloseTrades(tickers, portfolio)
3062
3063            else:
3064                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3066    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3067        """
3068        Universal method to create market or limit orders with all available parameters for current `accountId`.
3069        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3070
3071        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3072        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3073
3074        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3075        then broker immediately open market order as you can do simple --buy or --sell operations!
3076
3077        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3078        When current price will go up or down to target price value then broker opens a limit order.
3079        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3080
3081        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3082
3083        :param operation: string "Buy" or "Sell".
3084        :param orderType: string "Limit" or "Stop".
3085        :param lots: volume, integer count of lots >= 1.
3086        :param targetPrice: target price > 0. This is open trade price for limit order.
3087        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3088                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3089        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3090                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3091                         Stop loss order always executed by market price.
3092        :param expDate: string "Undefined" by default or local date in future.
3093                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3094                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3095                        A limit order has no expiration date, it lasts until the end of the trading day.
3096        :return: JSON with response from broker server.
3097        """
3098        if self.accountId is None or not self.accountId:
3099            uLogger.error("Variable `accountId` must be defined for using this method!")
3100            raise Exception("Account ID required")
3101
3102        if operation is None or not operation or operation not in ("Buy", "Sell"):
3103            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3104            raise Exception("Incorrect value")
3105
3106        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3107            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3108            raise Exception("Incorrect value")
3109
3110        if lots is None or lots < 1:
3111            uLogger.error("You must define trade volume > 0: integer count of lots!")
3112            raise Exception("Incorrect value")
3113
3114        if targetPrice is None or targetPrice <= 0:
3115            uLogger.error("Target price for limit-order must be greater than 0!")
3116            raise Exception("Incorrect value")
3117
3118        if limitPrice is None or limitPrice <= 0:
3119            limitPrice = targetPrice
3120
3121        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3122            stopType = "Limit"
3123
3124        if expDate is None or not expDate:
3125            expDate = "Undefined"
3126
3127        if not (self._ticker or self._figi):
3128            uLogger.error("Tocker or FIGI must be defined!")
3129            raise Exception("Ticker or FIGI required")
3130
3131        response = {}
3132        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3133        self._ticker = instrument["ticker"]
3134        self._figi = instrument["figi"]
3135
3136        if orderType == "Limit":
3137            uLogger.debug(
3138                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3139                    self._ticker, self._figi,
3140                    operation, lots, targetPrice, instrument["currency"],
3141                ))
3142
3143            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3144            self.body = str({
3145                "figi": self._figi,
3146                "quantity": str(lots),
3147                "price": FloatToNano(targetPrice),
3148                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3149                "accountId": str(self.accountId),
3150                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3151            })
3152            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3153
3154            if "orderId" in response.keys():
3155                uLogger.info(
3156                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3157                        response["orderId"],
3158                        self._ticker, self._figi,
3159                        operation, lots, targetPrice, instrument["currency"],
3160                    ))
3161
3162                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3163                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3164                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3165                            targetPrice, instrument["currency"],
3166                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3167                        ))
3168
3169                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3170                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3171                            targetPrice, instrument["currency"],
3172                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3173                        ))
3174
3175            else:
3176                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3177
3178        if orderType == "Stop":
3179            uLogger.debug(
3180                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3181                    self._ticker, self._figi,
3182                    operation, lots,
3183                    targetPrice, instrument["currency"],
3184                    limitPrice, instrument["currency"],
3185                    stopType, expDate,
3186                ))
3187
3188            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3189            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3190            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3191
3192            body = {
3193                "figi": self._figi,
3194                "quantity": str(lots),
3195                "price": FloatToNano(limitPrice),
3196                "stopPrice": FloatToNano(targetPrice),
3197                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3198                "accountId": str(self.accountId),
3199                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3200                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3201            }
3202
3203            if expDateUTC:
3204                body["expireDate"] = expDateUTC
3205
3206            self.body = str(body)
3207            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3208
3209            if "stopOrderId" in response.keys():
3210                uLogger.info(
3211                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3212                        response["stopOrderId"],
3213                        self._ticker, self._figi,
3214                        operation, lots,
3215                        targetPrice, instrument["currency"],
3216                        limitPrice, instrument["currency"],
3217                        TKS_STOP_ORDER_TYPES[stopOrderType],
3218                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3219                    ))
3220
3221                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3222                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3223                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3224                            targetPrice, instrument["currency"],
3225                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3226                        ))
3227
3228                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3229                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3230                            targetPrice, instrument["currency"],
3231                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3232                        ))
3233
3234            else:
3235                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3236
3237        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3239    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3240        """
3241        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3242        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3243        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3244        See also: `Order()` docstring.
3245
3246        :param lots: volume, integer count of lots >= 1.
3247        :param targetPrice: target price > 0. This is open trade price for limit order.
3248        :return: JSON with response from broker server.
3249        """
3250        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3252    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3253        """
3254        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3255        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3256        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3257        target price value then broker opens a limit order. See also: `Order()` docstring.
3258
3259        :param lots: volume, integer count of lots >= 1.
3260        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3261        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3262                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3263        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3264                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3265        :param expDate: string "Undefined" by default or local date in future.
3266                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3267                        This date is converting to UTC format for server.
3268        :return: JSON with response from broker server.
3269        """
3270        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3272    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3273        """
3274        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3275        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3276        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3277        See also: `Order()` docstring.
3278
3279        :param lots: volume, integer count of lots >= 1.
3280        :param targetPrice: target price > 0. This is open trade price for limit order.
3281        :return: JSON with response from broker server.
3282        """
3283        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3285    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3286        """
3287        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3288        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3289        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3290        target price value then broker opens a limit order. See also: `Order()` docstring.
3291
3292        :param lots: volume, integer count of lots >= 1.
3293        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3294        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3295                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3296        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3297                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3298        :param expDate: string "Undefined" by default or local date in future.
3299                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3300                        This date is converting to UTC format for server.
3301        :return: JSON with response from broker server.
3302        """
3303        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3305    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3306        """
3307        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3308
3309        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3310        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3311                             This avoids unnecessary downloading data from the server.
3312        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3313        """
3314        if self.accountId is None or not self.accountId:
3315            uLogger.error("Variable `accountId` must be defined for using this method!")
3316            raise Exception("Account ID required")
3317
3318        if orderIDs:
3319            if allOrdersIDs is None:
3320                rawOrders = self.RequestPendingOrders()
3321                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3322
3323            if allStopOrdersIDs is None:
3324                rawStopOrders = self.RequestStopOrders()
3325                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3326
3327            for orderID in orderIDs:
3328                idInPendingOrders = orderID in allOrdersIDs
3329                idInStopOrders = orderID in allStopOrdersIDs
3330
3331                if not (idInPendingOrders or idInStopOrders):
3332                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3333                    continue
3334
3335                else:
3336                    if idInPendingOrders:
3337                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3338
3339                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3340                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3341                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3342                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3343
3344                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3345                            if self.moreDebug:
3346                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3347
3348                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3349
3350                        else:
3351                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3352
3353                    elif idInStopOrders:
3354                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3355
3356                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3357                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3358                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3359                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3360
3361                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3362                            if self.moreDebug:
3363                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3364
3365                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3366
3367                        else:
3368                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3369
3370                    else:
3371                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3373    def CloseAllOrders(self) -> None:
3374        """
3375        Gets a list of open pending and stop orders and cancel it all.
3376        """
3377        rawOrders = self.RequestPendingOrders()
3378        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3379        lenOrders = len(allOrdersIDs)
3380
3381        rawStopOrders = self.RequestStopOrders()
3382        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3383        lenSOrders = len(allStopOrdersIDs)
3384
3385        if lenOrders > 0 or lenSOrders > 0:
3386            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3387
3388            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3389
3390        else:
3391            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3393    def CloseAll(self, *args) -> None:
3394        """
3395        Close all available (not blocked) opened trades and orders.
3396
3397        Also, you can select one or more keywords case-insensitive:
3398        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3399
3400        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3401        """
3402        overview = self.Overview(show=False)  # get all open trades info
3403
3404        if len(args) == 0:
3405            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3406            self.CloseAllOrders()  # close all pending and stop orders
3407
3408            for iType in TKS_INSTRUMENTS:
3409                if iType != "Currencies":
3410                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3411
3412        else:
3413            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3414            lowerArgs = [x.lower() for x in args]
3415
3416            if "orders" in lowerArgs:
3417                self.CloseAllOrders()  # close all pending and stop orders
3418
3419            for iType in TKS_INSTRUMENTS:
3420                if iType.lower() in lowerArgs and iType != "Currencies":
3421                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

def CloseAllByTicker(self, instrument: str) -> None:
3423    def CloseAllByTicker(self, instrument: str) -> None:
3424        """
3425        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3426
3427        This method searches opened trade and orders of instrument throw all portfolio and then use
3428        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3429
3430        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3431
3432        :param instrument: string with ticker.
3433        """
3434        if instrument is None or not instrument:
3435            uLogger.error("Ticker name must be defined for using this method!")
3436            raise Exception("Ticker required")
3437
3438        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3439
3440        self._ticker = instrument  # try to set instrument as ticker
3441        self._figi = ""
3442
3443        if self.IsInPortfolio(portfolio=overview):
3444            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3445            self.CloseTrades(instruments=[instrument], portfolio=overview)
3446
3447        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3448        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3449
3450        if limitAll and self.IsInLimitOrders(portfolio=overview):
3451            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3452            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3453
3454        if stopAll and self.IsInStopOrders(portfolio=overview):
3455            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3456            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)

Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with ticker.
def CloseAllByFIGI(self, instrument: str) -> None:
3458    def CloseAllByFIGI(self, instrument: str) -> None:
3459        """
3460        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3461
3462        This method searches opened trade and orders of instrument throw all portfolio and then use
3463        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3464
3465        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3466
3467        :param instrument: string with FIGI id.
3468        """
3469        if instrument is None or not instrument:
3470            uLogger.error("FIGI id must be defined for using this method!")
3471            raise Exception("FIGI required")
3472
3473        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3474
3475        self._ticker = ""
3476        self._figi = instrument  # try to set instrument as FIGI id
3477
3478        if self.IsInPortfolio(portfolio=overview):
3479            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3480            self.CloseTrades(instruments=[instrument], portfolio=overview)
3481
3482        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3483        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3484
3485        if limitAll and self.IsInLimitOrders(portfolio=overview):
3486            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3487            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3488
3489        if stopAll and self.IsInStopOrders(portfolio=overview):
3490            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3491            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)

Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with FIGI id.
@staticmethod
def ParseOrderParameters(operation, **inputParameters)
3493    @staticmethod
3494    def ParseOrderParameters(operation, **inputParameters):
3495        """
3496        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3497
3498        :param operation: string "Buy" or "Sell".
3499        :param inputParameters: this is dict of strings that looks like this
3500               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3501               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3502               "prices" key: one or more prices to open limit-orders
3503               Counts of values in lots and prices lists must be equals!
3504        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3505        """
3506        # TODO: update order grid work with api v2
3507        pass
3508        # uLogger.debug("Input parameters: {}".format(inputParameters))
3509        #
3510        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3511        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3512        #     raise Exception("Incorrect value")
3513        #
3514        # if "l" in inputParameters.keys():
3515        #     inputParameters["lots"] = inputParameters.pop("l")
3516        #
3517        # if "p" in inputParameters.keys():
3518        #     inputParameters["prices"] = inputParameters.pop("p")
3519        #
3520        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3521        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3522        #     raise Exception("Incorrect value")
3523        #
3524        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3525        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3526        #
3527        # if len(lots) != len(prices):
3528        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3529        #     raise Exception("Incorrect value")
3530        #
3531        # uLogger.debug("Extracted parameters for orders:")
3532        # uLogger.debug("lots = {}".format(lots))
3533        # uLogger.debug("prices = {}".format(prices))
3534        #
3535        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3536        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3537        # uLogger.debug("Order parameters: {}".format(result))
3538        #
3539        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3541    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3542        """
3543        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3544
3545        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3546        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3547        """
3548        result = False
3549        msg = "Instrument not defined!"
3550
3551        if portfolio is None or not portfolio:
3552            portfolio = self.Overview(show=False)
3553
3554        if self._ticker:
3555            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3556            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3557
3558            for iType in TKS_INSTRUMENTS:
3559                for instrument in portfolio["stat"][iType]:
3560                    if instrument["ticker"] == self._ticker:
3561                        result = True
3562                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3563                        break
3564
3565        elif self._figi:
3566            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3567            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3568
3569            for iType in TKS_INSTRUMENTS:
3570                for instrument in portfolio["stat"][iType]:
3571                    if instrument["figi"] == self._figi:
3572                        result = True
3573                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3574                        break
3575
3576        else:
3577            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3578
3579        uLogger.debug(msg)
3580
3581        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3583    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3584        """
3585        Returns instrument from the user's portfolio if it presents there.
3586        Instrument must be defined by `ticker` (highly priority) or `figi`.
3587
3588        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3589        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3590        """
3591        result = None
3592        msg = "Instrument not defined!"
3593
3594        if portfolio is None or not portfolio:
3595            portfolio = self.Overview(show=False)
3596
3597        if self._ticker:
3598            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3599            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3600
3601            for iType in TKS_INSTRUMENTS:
3602                for instrument in portfolio["stat"][iType]:
3603                    if instrument["ticker"] == self._ticker:
3604                        result = instrument
3605                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3606                        break
3607
3608        elif self._figi:
3609            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3610            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3611
3612            for iType in TKS_INSTRUMENTS:
3613                for instrument in portfolio["stat"][iType]:
3614                    if instrument["figi"] == self._figi:
3615                        result = instrument
3616                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3617                        break
3618
3619        else:
3620            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3621
3622        uLogger.debug(msg)
3623
3624        return result

Returns instrument from the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3626    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3627        """
3628        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3629
3630        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3631
3632        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3633        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3634        """
3635        result = False
3636        msg = "Instrument not defined!"
3637
3638        if portfolio is None or not portfolio:
3639            portfolio = self.Overview(show=False)
3640
3641        if self._ticker:
3642            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3643            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3644
3645            for instrument in portfolio["stat"]["orders"]:
3646                if instrument["ticker"] == self._ticker:
3647                    result = True
3648                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3649                    break
3650
3651        elif self._figi:
3652            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3653            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3654
3655            for instrument in portfolio["stat"]["orders"]:
3656                if instrument["figi"] == self._figi:
3657                    result = True
3658                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3659                    break
3660
3661        else:
3662            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3663
3664        uLogger.debug(msg)
3665
3666        return result

Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if limit orders list contains some limit orders for the instrument, False otherwise.

def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3668    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3669        """
3670        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3671        Instrument must be defined by `ticker` (highly priority) or `figi`.
3672
3673        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3674
3675        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3676        :return: list with `orderID`s of limit orders.
3677        """
3678        result = []
3679        msg = "Instrument not defined!"
3680
3681        if portfolio is None or not portfolio:
3682            portfolio = self.Overview(show=False)
3683
3684        if self._ticker:
3685            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3686            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3687
3688            for instrument in portfolio["stat"]["orders"]:
3689                if instrument["ticker"] == self._ticker:
3690                    result.append(instrument["orderID"])
3691
3692            if result:
3693                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3694
3695        elif self._figi:
3696            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3697            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3698
3699            for instrument in portfolio["stat"]["orders"]:
3700                if instrument["figi"] == self._figi:
3701                    result.append(instrument["orderID"])
3702
3703            if result:
3704                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3705
3706        else:
3707            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3708
3709        uLogger.debug(msg)
3710
3711        return result

Returns list with all orderIDs of opened pending limit orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of limit orders.

def IsInStopOrders(self, portfolio: dict = None) -> bool:
3713    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3714        """
3715        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3716
3717        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3718
3719        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3720        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3721        """
3722        result = False
3723        msg = "Instrument not defined!"
3724
3725        if portfolio is None or not portfolio:
3726            portfolio = self.Overview(show=False)
3727
3728        if self._ticker:
3729            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3730            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3731
3732            for instrument in portfolio["stat"]["stopOrders"]:
3733                if instrument["ticker"] == self._ticker:
3734                    result = True
3735                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3736                    break
3737
3738        elif self._figi:
3739            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3740            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3741
3742            for instrument in portfolio["stat"]["stopOrders"]:
3743                if instrument["figi"] == self._figi:
3744                    result = True
3745                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3746                    break
3747
3748        else:
3749            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3750
3751        uLogger.debug(msg)
3752
3753        return result

Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if stop orders list contains some stop orders for the instrument, False otherwise.

def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3755    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3756        """
3757        Returns list with all `orderID`s of opened stop orders for the instrument.
3758        Instrument must be defined by `ticker` (highly priority) or `figi`.
3759
3760        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3761
3762        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3763        :return: list with `orderID`s of stop orders.
3764        """
3765        result = []
3766        msg = "Instrument not defined!"
3767
3768        if portfolio is None or not portfolio:
3769            portfolio = self.Overview(show=False)
3770
3771        if self._ticker:
3772            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3773            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3774
3775            for instrument in portfolio["stat"]["stopOrders"]:
3776                if instrument["ticker"] == self._ticker:
3777                    result.append(instrument["orderID"])
3778
3779            if result:
3780                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3781
3782        elif self._figi:
3783            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3784            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3785
3786            for instrument in portfolio["stat"]["stopOrders"]:
3787                if instrument["figi"] == self._figi:
3788                    result.append(instrument["orderID"])
3789
3790            if result:
3791                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3792
3793        else:
3794            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3795
3796        uLogger.debug(msg)
3797
3798        return result

Returns list with all orderIDs of opened stop orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of stop orders.

def RequestLimits(self) -> dict:
3800    def RequestLimits(self) -> dict:
3801        """
3802        Method for obtaining the available funds for withdrawal for current `accountId`.
3803
3804        See also:
3805        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3806        - `OverviewLimits()` method
3807
3808        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3809                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3810                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3811                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3812        """
3813        if self.accountId is None or not self.accountId:
3814            uLogger.error("Variable `accountId` must be defined for using this method!")
3815            raise Exception("Account ID required")
3816
3817        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3818
3819        self.body = str({"accountId": self.accountId})
3820        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3821        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3822
3823        if self.moreDebug:
3824            uLogger.debug("Records about available funds for withdrawal successfully received")
3825
3826        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False) -> dict:
3828    def OverviewLimits(self, show: bool = False) -> dict:
3829        """
3830        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3831
3832        See also: `RequestLimits()`.
3833
3834        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3835        :return: dict with raw parsed data from server and some calculated statistics about it.
3836        """
3837        if self.accountId is None or not self.accountId:
3838            uLogger.error("Variable `accountId` must be defined for using this method!")
3839            raise Exception("Account ID required")
3840
3841        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3842
3843        view = {
3844            "rawLimits": rawLimits,
3845            "limits": {  # parsed data for every currency:
3846                "money": {  # this is an array of portfolio currency positions
3847                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3848                },
3849                "blocked": {  # this is an array of blocked currency
3850                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3851                },
3852                "blockedGuarantee": {  # this is locked money under collateral for futures
3853                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3854                },
3855            },
3856        }
3857
3858        # --- Prepare text table with limits in human-readable format:
3859        if show:
3860            info = [
3861                "# Withdrawal limits\n\n",
3862                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3863                "* **Account ID:** [{}]\n".format(self.accountId),
3864            ]
3865
3866            if view["limits"]["money"]:
3867                info.extend([
3868                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3869                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3870                ])
3871
3872            else:
3873                info.append("\nNo withdrawal limits\n")
3874
3875            for curr in view["limits"]["money"].keys():
3876                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3877                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3878                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3879
3880                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3881                    "[{}]".format(curr),
3882                    "{:.2f}".format(view["limits"]["money"][curr]),
3883                    "{:.2f}".format(availableMoney),
3884                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3885                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3886                )
3887
3888                if curr == "rub":
3889                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3890
3891                else:
3892                    info.append(infoStr)
3893
3894            infoText = "".join(info)
3895
3896            uLogger.info(infoText)
3897
3898            if self.withdrawalLimitsFile:
3899                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3900                    fH.write(infoText)
3901
3902                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3903
3904        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
3906    def RequestAccounts(self) -> dict:
3907        """
3908        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3909
3910        See also:
3911        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3912        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3913        - `OverviewUserInfo()` method
3914
3915        :return: dict with raw data from server that contains accounts info. Example of dict:
3916                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3917                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3918                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3919                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3920        """
3921        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3922
3923        self.body = str({})
3924        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3925        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3926
3927        if self.moreDebug:
3928            uLogger.debug("Records about available accounts successfully received")
3929
3930        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
3932    def RequestUserInfo(self) -> dict:
3933        """
3934        Method for requesting common user's information.
3935
3936        See also:
3937        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3938        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3939        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3940        - `OverviewUserInfo()` method
3941
3942        :return: dict with raw data from server that contains user's information. Example of dict:
3943                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3944                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3945        """
3946        uLogger.debug("Requesting common user's information. Wait, please...")
3947
3948        self.body = str({})
3949        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3950        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3951
3952        if self.moreDebug:
3953            uLogger.debug("Records about current user successfully received")
3954
3955        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
3957    def RequestMarginStatus(self, accountId: str = None) -> dict:
3958        """
3959        Method for requesting margin calculation for defined account ID.
3960
3961        See also:
3962        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3963        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3964        - `OverviewUserInfo()` method
3965
3966        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3967        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3968                 Example of responses:
3969                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3970                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3971                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3972                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3973                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3974                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3975        """
3976        if accountId is None or not accountId:
3977            if self.accountId is None or not self.accountId:
3978                uLogger.error("Variable `accountId` must be defined for using this method!")
3979                raise Exception("Account ID required")
3980
3981            else:
3982                accountId = self.accountId  # use `self.accountId` (main ID) by default
3983
3984        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3985
3986        self.body = str({"accountId": accountId})
3987        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3988        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3989
3990        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3991            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3992            rawMargin = {}
3993
3994        else:
3995            if self.moreDebug:
3996                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3997
3998        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
4000    def RequestTariffLimits(self) -> dict:
4001        """
4002        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4003
4004        See also:
4005        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4006        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4007        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4008        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4009        - `OverviewUserInfo()` method
4010
4011        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4012                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4013                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4014        """
4015        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4016
4017        self.body = str({})
4018        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4019        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4020
4021        if self.moreDebug:
4022            uLogger.debug("Records with limits of current tariff successfully received")
4023
4024        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
4026    def RequestBondCoupons(self, iJSON: dict) -> dict:
4027        """
4028        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4029        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4030        All dates are in UTC timezone.
4031
4032        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4033        Documentation:
4034        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4035        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4036
4037        See also: `ExtendBondsData()`.
4038
4039        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4040                      If raw iJSON is not data of bond then server returns an error [400] with message:
4041                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4042        :return: dictionary with bond payment calendar. Response example
4043                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4044                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4045                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4046                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4047        """
4048        if iJSON["figi"] is None or not iJSON["figi"]:
4049            uLogger.error("FIGI must be defined for using this method!")
4050            raise Exception("FIGI required")
4051
4052        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4053        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4054
4055        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4056            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4057            self._figi,
4058            startDate,
4059            endDate,
4060        ))
4061
4062        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4063        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4064        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4065
4066        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4067            uLogger.warning("Instrument type is not bond!")
4068
4069        else:
4070            if self.moreDebug:
4071                uLogger.debug("Records about bond payment calendar successfully received")
4072
4073        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self._ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
4075    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4076        """
4077        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4078        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4079        coupon yields, current yields and some statistics etc.
4080
4081        WARNING! This is too long operation if a lot of bonds requested from broker server.
4082
4083        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4084
4085        :param instruments: list of strings with tickers or FIGIs.
4086        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4087                     for further used by data scientists or stock analytics.
4088        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4089                 In XLSX-file and Pandas DataFrame fields mean:
4090                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4091                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4092        """
4093        if instruments is None or not instruments:
4094            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4095            raise Exception("Ticker or FIGI required")
4096
4097        if isinstance(instruments, str):
4098            instruments = [instruments]
4099
4100        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4101
4102        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4103
4104        iCount = len(uniqueInstruments)
4105        tooLong = iCount >= 20
4106        if tooLong:
4107            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4108
4109        bonds = None
4110        for i, self._figi in enumerate(uniqueInstruments):
4111            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4112
4113            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4114                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4115                rawBond = self.SearchByFIGI(requestPrice=True)
4116
4117                # Widen raw data with UTC current time (iData["actualDateTime"]):
4118                actualDate = datetime.now(tzutc())
4119                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4120
4121                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4122                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4123
4124                # Replace some values with human-readable:
4125                iData["nominalCurrency"] = iData["nominal"]["currency"]
4126                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4127                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4128                iData["aciCurrency"] = iData["aciValue"]["currency"]
4129                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4130                iData["issueSize"] = int(iData["issueSize"])
4131                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4132                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4133                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4134                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4135                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4136                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4137                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4138                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4139                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4140                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4141
4142                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4143                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4144                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4145                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4146                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4147                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4148                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4149                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4150                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4151                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4152                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4153
4154                # Widen raw data with calendar data from `rawCalendar` values:
4155                calendarData = []
4156                if "events" in iData["rawCalendar"].keys():
4157                    for item in iData["rawCalendar"]["events"]:
4158                        calendarData.append({
4159                            "couponDate": item["couponDate"],
4160                            "couponNumber": int(item["couponNumber"]),
4161                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4162                            "payCurrency": item["payOneBond"]["currency"],
4163                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4164                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4165                            "couponStartDate": item["couponStartDate"],
4166                            "couponEndDate": item["couponEndDate"],
4167                            "couponPeriod": item["couponPeriod"],
4168                        })
4169
4170                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4171                    if "maturityDate" not in iData.keys():
4172                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4173
4174                # Widen raw data with Coupon Rate.
4175                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4176                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4177                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4178                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4179
4180                # Widen raw data with Yield to Maturity (YTM) on current date.
4181                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4182                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4183                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4184                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4185                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4186                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4187
4188                iData["calendar"] = calendarData  # adds calendar at the end
4189
4190                # Remove not used data:
4191                iData.pop("uid")
4192                iData.pop("positionUid")
4193                iData.pop("currentPrice")
4194                iData.pop("rawCalendar")
4195
4196                colNames = list(iData.keys())
4197                if bonds is None:
4198                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4199
4200                else:
4201                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4202
4203            else:
4204                uLogger.warning("Instrument is not a bond!")
4205
4206            processed = round(100 * (i + 1) / iCount, 1)
4207            if tooLong and processed % 5 == 0:
4208                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4209
4210            else:
4211                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4212
4213        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4214
4215        # Saving bonds from Pandas DataFrame to XLSX sheet:
4216        if xlsx and self.bondsXLSXFile:
4217            with pd.ExcelWriter(
4218                    path=self.bondsXLSXFile,
4219                    date_format=TKS_DATE_FORMAT,
4220                    datetime_format=TKS_DATE_TIME_FORMAT,
4221                    mode="w",
4222            ) as writer:
4223                bonds.to_excel(
4224                    writer,
4225                    sheet_name="Extended bonds data",
4226                    index=True,
4227                    encoding="UTF-8",
4228                    freeze_panes=(1, 1),
4229                )  # saving as XLSX-file with freeze first row and column as headers
4230
4231            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4232
4233        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
4235    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4236        """
4237        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4238
4239        WARNING! This is too long operation if a lot of bonds requested from broker server.
4240
4241        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4242
4243        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4244                        extended information about bonds: main info, current prices, bond payment calendar,
4245                        coupon yields, current yields and some statistics etc.
4246                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4247        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4248                     for further used by data scientists or stock analytics.
4249        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4250        """
4251        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4252            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4253
4254        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4255
4256        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4257        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4258        calendar = None
4259        for bond in extBonds.iterrows():
4260            for item in bond[1]["calendar"]:
4261                cData = {
4262                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4263                    "couponDate": item["couponDate"],
4264                    "figi": bond[1]["figi"],
4265                    "ticker": bond[1]["ticker"],
4266                    "name": bond[1]["name"],
4267                    "couponNumber": item["couponNumber"],
4268                    "payOneBond": item["payOneBond"],
4269                    "payCurrency": item["payCurrency"],
4270                    "couponType": item["couponType"],
4271                    "couponPeriod": item["couponPeriod"],
4272                    "fixDate": item["fixDate"],
4273                    "couponStartDate": item["couponStartDate"],
4274                    "couponEndDate": item["couponEndDate"],
4275                }
4276
4277                if calendar is None:
4278                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4279
4280                else:
4281                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4282
4283        if calendar is not None:
4284            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4285
4286            # Saving calendar from Pandas DataFrame to XLSX sheet:
4287            if xlsx:
4288                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4289
4290                with pd.ExcelWriter(
4291                        path=xlsxCalendarFile,
4292                        date_format=TKS_DATE_FORMAT,
4293                        datetime_format=TKS_DATE_TIME_FORMAT,
4294                        mode="w",
4295                ) as writer:
4296                    humanReadable = calendar.copy(deep=True)
4297                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4298                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4299                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4300                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4301                    humanReadable.columns = colNames  # human-readable column names
4302
4303                    humanReadable.to_excel(
4304                        writer,
4305                        sheet_name="Bond payments calendar",
4306                        index=False,
4307                        encoding="UTF-8",
4308                        freeze_panes=(1, 2),
4309                    )  # saving as XLSX-file with freeze first row and column as headers
4310
4311                    del humanReadable  # release df in memory
4312
4313                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4314
4315        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar(self, extBonds: pandas.core.frame.DataFrame, show: bool = True) -> str:
4317    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4318        """
4319        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4320        Also, creates Markdown file with calendar data, `calendar.md` by default.
4321
4322        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4323
4324        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4325                        extended information about bonds: main info, current prices, bond payment calendar,
4326                        coupon yields, current yields and some statistics etc.
4327                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4328        :param show: if `True` then also printing bonds payment calendar to the console,
4329                     otherwise save to file `calendarFile` only. `False` by default.
4330        :return: multilines text in Markdown format with bonds payment calendar as a table.
4331        """
4332        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4333            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4334
4335        infoText = "# Bond payments calendar\n\n"
4336
4337        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4338
4339        if not (calendar is None or calendar.empty):
4340            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4341
4342            info = [
4343                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4344                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4345            ]
4346
4347            newMonth = False
4348            notOneBond = calendar["figi"].nunique() > 1
4349            for i, bond in enumerate(calendar.iterrows()):
4350                if newMonth and notOneBond:
4351                    info.append(splitLine)
4352
4353                info.append(
4354                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4355                        "  √" if bond[1]["paid"] else "  —",
4356                        bond[1]["couponDate"].split("T")[0],
4357                        bond[1]["figi"],
4358                        bond[1]["ticker"],
4359                        bond[1]["couponNumber"],
4360                        "{} {}".format(
4361                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4362                            bond[1]["payCurrency"],
4363                        ),
4364                        bond[1]["couponType"],
4365                        bond[1]["couponPeriod"],
4366                        bond[1]["fixDate"].split("T")[0],
4367                    )
4368                )
4369
4370                if i < len(calendar.values) - 1:
4371                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4372                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4373                    newMonth = False if curDate.month == nextDate.month else True
4374
4375                else:
4376                    newMonth = False
4377
4378            infoText += "".join(info)
4379
4380            if show:
4381                uLogger.info("{}".format(infoText))
4382
4383            if self.calendarFile is not None:
4384                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4385                    fH.write(infoText)
4386
4387                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4388
4389        else:
4390            infoText += "No data\n"
4391
4392        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False) -> dict:
4394    def OverviewAccounts(self, show: bool = False) -> dict:
4395        """
4396        Method for parsing and show simple table with all available user accounts.
4397
4398        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4399
4400        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4401        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4402                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4403                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4404                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4405                                                        "closed": "—", "access": "Full access" }, ...}}`
4406        """
4407        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4408
4409        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4410        accounts = {
4411            item["id"]: {
4412                "type": TKS_ACCOUNT_TYPES[item["type"]],
4413                "name": item["name"],
4414                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4415                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4416                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4417                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4418            } for item in rawAccounts["accounts"]
4419        }
4420
4421        # Raw and parsed data with some fields replaced in "stat" section:
4422        view = {
4423            "rawAccounts": rawAccounts,
4424            "stat": accounts,
4425        }
4426
4427        # --- Prepare simple text table with only accounts data in human-readable format:
4428        if show:
4429            info = [
4430                "# User accounts\n\n",
4431                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4432                "| Account ID   | Type                      | Status                    | Name                           |\n",
4433                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4434            ]
4435
4436            for account in view["stat"].keys():
4437                info.extend([
4438                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4439                        account,
4440                        view["stat"][account]["type"],
4441                        view["stat"][account]["status"],
4442                        view["stat"][account]["name"],
4443                    )
4444                ])
4445
4446            infoText = "".join(info)
4447
4448            uLogger.info(infoText)
4449
4450            if self.userAccountsFile:
4451                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4452                    fH.write(infoText)
4453
4454                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4455
4456        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False) -> dict:
4458    def OverviewUserInfo(self, show: bool = False) -> dict:
4459        """
4460        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4461
4462        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4463
4464        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4465        :return: dict with raw parsed data from server and some calculated statistics about it.
4466        """
4467        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4468        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4469        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4470        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4471        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4472        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4473
4474        # This is dict with parsed common user data:
4475        userInfo = {
4476            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4477            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4478            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4479            "tariff": rawUserInfo["tariff"],
4480        }
4481
4482        # This is an array of dict with parsed margin statuses for every account IDs:
4483        margins = {}
4484        for accountId in accounts.keys():
4485            if rawMargins[accountId]:
4486                margins[accountId] = {
4487                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4488                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4489                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4490                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4491                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4492                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4493                }
4494
4495            else:
4496                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4497
4498        unary = {}  # unary-connection limits
4499        for item in rawTariffLimits["unaryLimits"]:
4500            if item["limitPerMinute"] in unary.keys():
4501                unary[item["limitPerMinute"]].extend(item["methods"])
4502
4503            else:
4504                unary[item["limitPerMinute"]] = item["methods"]
4505
4506        stream = {}  # stream-connection limits
4507        for item in rawTariffLimits["streamLimits"]:
4508            if item["limit"] in stream.keys():
4509                stream[item["limit"]].extend(item["streams"])
4510
4511            else:
4512                stream[item["limit"]] = item["streams"]
4513
4514        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4515        limits = {
4516            "unary": unary,
4517            "stream": stream,
4518        }
4519
4520        # Raw and parsed data as an output result:
4521        view = {
4522            "rawUserInfo": rawUserInfo,
4523            "rawAccounts": rawAccounts,
4524            "rawMargins": rawMargins,
4525            "rawTariffLimits": rawTariffLimits,
4526            "stat": {
4527                "userInfo": userInfo,
4528                "accounts": accounts,
4529                "margins": margins,
4530                "limits": limits,
4531            },
4532        }
4533
4534        # --- Prepare text table with user information in human-readable format:
4535        if show:
4536            info = [
4537                "# Full user information\n\n",
4538                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4539                "## Common information\n\n",
4540                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4541                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4542                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4543                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4544                "\n## User accounts\n\n",
4545            ]
4546
4547            for account in view["stat"]["accounts"].keys():
4548                info.extend([
4549                    "### ID: [{}]\n\n".format(account),
4550                    "| Parameters           | Values                                                       |\n",
4551                    "|----------------------|--------------------------------------------------------------|\n",
4552                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4553                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4554                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4555                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4556                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4557                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4558                ])
4559
4560                if margins[account]:
4561                    info.extend([
4562                        "| Margin status:       | Enabled                                                      |\n",
4563                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4564                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4565                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4566                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4567                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4568                    ])
4569
4570                else:
4571                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4572
4573            info.extend([
4574                "\n## Current user tariff limits\n",
4575                "\nSee also:\n",
4576                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4577                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4578                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4579                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4580                "\n### Unary limits\n",
4581            ])
4582
4583            if unary:
4584                for key, values in sorted(unary.items()):
4585                    info.append("\n* Max requests per minute: {}\n".format(key))
4586
4587                    for value in values:
4588                        info.append("  - {}\n".format(value))
4589
4590            else:
4591                info.append("\nNot available\n")
4592
4593            info.append("\n### Stream limits\n")
4594
4595            if stream:
4596                for key, values in sorted(stream.items()):
4597                    info.append("\n* Max stream connections: {}\n".format(key))
4598
4599                    for value in values:
4600                        info.append("  - {}\n".format(value))
4601
4602            else:
4603                info.append("\nNot available\n")
4604
4605            infoText = "".join(info)
4606
4607            uLogger.info(infoText)
4608
4609            if self.userInfoFile:
4610                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4611                    fH.write(infoText)
4612
4613                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4614
4615        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4618class Args:
4619    """
4620    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4621    """
4622    def __init__(self, **kwargs):
4623        self.__dict__.update(kwargs)
4624
4625    def __getattr__(self, item):
4626        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4622    def __init__(self, **kwargs):
4623        self.__dict__.update(kwargs)
def ParseArgs()
4629def ParseArgs():
4630    """This function get and parse command line keys."""
4631    parser = ArgumentParser()  # command-line string parser
4632
4633    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4634    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4635
4636    # --- options:
4637
4638    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4639    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4640    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4641
4642    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4643    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4644
4645    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4646    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4647
4648    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4649
4650    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4651    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4652    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4653
4654    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4655    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4656
4657    # --- commands:
4658
4659    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4660
4661    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4662    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4663    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4664    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4665    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4666    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4667    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4668    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4669
4670    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4671    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4672    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4673    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4674    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4675    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4676
4677    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4678    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4679    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4680    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4681
4682    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4683    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4684    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4685
4686    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4687    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4688    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4689    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4690    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4691    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4692    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4693
4694    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4695    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4696    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4697    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4698    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4699
4700    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4701    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4702    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4703
4704    cmdArgs = parser.parse_args()
4705    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs)
4708def Main(**kwargs):
4709    """
4710    Main function for work with TKSBrokerAPI in the console.
4711
4712    See examples:
4713    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4714    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4715    """
4716    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4717
4718    if args.debug_level:
4719        uLogger.level = 10  # always debug level by default
4720        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4721
4722    exitCode = 0
4723    start = datetime.now(tzutc())
4724    uLogger.debug("=-" * 50)
4725    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4726        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4727        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4728    ))
4729
4730    # trying to calculate full current version:
4731    buildVersion = __version__
4732    try:
4733        v = version("tksbrokerapi")
4734        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4735
4736    except Exception:
4737        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4738
4739    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4740    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4741
4742    try:
4743        if args.version:
4744            print("TKSBrokerAPI {}".format(buildVersion))
4745            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4746
4747        else:
4748            # Init class for trading with Tinkoff Broker:
4749            trader = TinkoffBrokerServer(
4750                token=args.token,
4751                accountId=args.account_id,
4752                useCache=not args.no_cache,
4753            )
4754
4755            # --- set some options:
4756
4757            if args.more:
4758                trader.moreDebug = True
4759                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4760
4761            if args.ticker:
4762                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4763
4764                if ticker in trader.aliasesKeys:
4765                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4766
4767                else:
4768                    trader.ticker = ticker
4769
4770            if args.figi:
4771                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4772
4773            if args.depth is not None:
4774                trader.depth = args.depth
4775
4776            # --- do one command:
4777
4778            if args.list:
4779                if args.output is not None:
4780                    trader.instrumentsFile = args.output
4781
4782                trader.ShowInstrumentsInfo(show=True)
4783
4784            elif args.list_xlsx:
4785                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4786
4787            elif args.bonds_xlsx is not None:
4788                if args.output is not None:
4789                    trader.bondsXLSXFile = args.output
4790
4791                if len(args.bonds_xlsx) == 0:
4792                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4793
4794                else:
4795                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4796
4797            elif args.search:
4798                if args.output is not None:
4799                    trader.searchResultsFile = args.output
4800
4801                trader.SearchInstruments(pattern=args.search[0], show=True)
4802
4803            elif args.info:
4804                if not (args.ticker or args.figi):
4805                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4806                    raise Exception("Ticker or FIGI required")
4807
4808                if args.output is not None:
4809                    trader.infoFile = args.output
4810
4811                if args.ticker:
4812                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4813
4814                else:
4815                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4816
4817            elif args.calendar is not None:
4818                if args.output is not None:
4819                    trader.calendarFile = args.output
4820
4821                if len(args.calendar) == 0:
4822                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4823
4824                else:
4825                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4826
4827                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4828
4829            elif args.price:
4830                if not (args.ticker or args.figi):
4831                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4832                    raise Exception("Ticker or FIGI required")
4833
4834                trader.GetCurrentPrices(show=True)
4835
4836            elif args.prices is not None:
4837                if args.output is not None:
4838                    trader.pricesFile = args.output
4839
4840                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4841
4842            elif args.overview:
4843                if args.output is not None:
4844                    trader.overviewFile = args.output
4845
4846                trader.Overview(show=True, details="full")
4847
4848            elif args.overview_digest:
4849                if args.output is not None:
4850                    trader.overviewDigestFile = args.output
4851
4852                trader.Overview(show=True, details="digest")
4853
4854            elif args.overview_positions:
4855                if args.output is not None:
4856                    trader.overviewPositionsFile = args.output
4857
4858                trader.Overview(show=True, details="positions")
4859
4860            elif args.overview_orders:
4861                if args.output is not None:
4862                    trader.overviewOrdersFile = args.output
4863
4864                trader.Overview(show=True, details="orders")
4865
4866            elif args.overview_analytics:
4867                if args.output is not None:
4868                    trader.overviewAnalyticsFile = args.output
4869
4870                trader.Overview(show=True, details="analytics")
4871
4872            elif args.overview_calendar:
4873                if args.output is not None:
4874                    trader.overviewAnalyticsFile = args.output
4875
4876                trader.Overview(show=True, details="calendar")
4877
4878            elif args.deals is not None:
4879                if args.output is not None:
4880                    trader.reportFile = args.output
4881
4882                if 0 <= len(args.deals) < 3:
4883                    trader.Deals(
4884                        start=args.deals[0] if len(args.deals) >= 1 else None,
4885                        end=args.deals[1] if len(args.deals) == 2 else None,
4886                        show=True,  # Always show deals report in console
4887                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4888                    )
4889
4890                else:
4891                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4892                    raise Exception("Incorrect value")
4893
4894            elif args.history is not None:
4895                if args.output is not None:
4896                    trader.historyFile = args.output
4897
4898                if 0 <= len(args.history) < 3:
4899                    dataReceived = trader.History(
4900                        start=args.history[0] if len(args.history) >= 1 else None,
4901                        end=args.history[1] if len(args.history) == 2 else None,
4902                        interval="hour" if args.interval is None or not args.interval else args.interval,
4903                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4904                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4905                        show=True,  # shows all downloaded candles in console
4906                    )
4907
4908                    if args.render_chart is not None and dataReceived is not None:
4909                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4910
4911                        trader.ShowHistoryChart(
4912                            candles=dataReceived,
4913                            interact=iChart,
4914                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4915                        )
4916
4917                else:
4918                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4919                    raise Exception("Incorrect value")
4920
4921            elif args.load_history is not None:
4922                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4923
4924                if args.render_chart is not None and histData is not None:
4925                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4926                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4927
4928                    trader.ShowHistoryChart(
4929                        candles=histData,
4930                        interact=iChart,
4931                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4932                    )
4933
4934            elif args.trade is not None:
4935                if 1 <= len(args.trade) <= 5:
4936                    trader.Trade(
4937                        operation=args.trade[0],
4938                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4939                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4940                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4941                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4942                    )
4943
4944                else:
4945                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4946
4947            elif args.buy is not None:
4948                if 0 <= len(args.buy) <= 4:
4949                    trader.Buy(
4950                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4951                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4952                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4953                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4954                    )
4955
4956                else:
4957                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4958
4959            elif args.sell is not None:
4960                if 0 <= len(args.sell) <= 4:
4961                    trader.Sell(
4962                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4963                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4964                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4965                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4966                    )
4967
4968                else:
4969                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4970
4971            elif args.order:
4972                if 4 <= len(args.order) <= 7:
4973                    trader.Order(
4974                        operation=args.order[0],
4975                        orderType=args.order[1],
4976                        lots=int(args.order[2]),
4977                        targetPrice=float(args.order[3]),
4978                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4979                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4980                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4981                    )
4982
4983                else:
4984                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4985
4986            elif args.buy_limit:
4987                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4988
4989            elif args.sell_limit:
4990                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4991
4992            elif args.buy_stop:
4993                if 2 <= len(args.buy_stop) <= 7:
4994                    trader.BuyStop(
4995                        lots=int(args.buy_stop[0]),
4996                        targetPrice=float(args.buy_stop[1]),
4997                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4998                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4999                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5000                    )
5001
5002                else:
5003                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5004
5005            elif args.sell_stop:
5006                if 2 <= len(args.sell_stop) <= 7:
5007                    trader.SellStop(
5008                        lots=int(args.sell_stop[0]),
5009                        targetPrice=float(args.sell_stop[1]),
5010                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5011                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5012                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5013                    )
5014
5015                else:
5016                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5017
5018            # elif args.buy_order_grid is not None:
5019            #     # update order grid work with api v2
5020            #     if len(args.buy_order_grid) == 2:
5021            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5022            #
5023            #         for order in orderParams:
5024            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5025            #
5026            #     else:
5027            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5028            #
5029            # elif args.sell_order_grid is not None:
5030            #     # update order grid work with api v2
5031            #     if len(args.sell_order_grid) >= 2:
5032            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5033            #
5034            #         for order in orderParams:
5035            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5036            #
5037            #     else:
5038            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5039
5040            elif args.close_order is not None:
5041                trader.CloseOrders(args.close_order)  # close only one order
5042
5043            elif args.close_orders is not None:
5044                trader.CloseOrders(args.close_orders)  # close list of orders
5045
5046            elif args.close_trade:
5047                if not (args.ticker or args.figi):
5048                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5049                    raise Exception("Ticker or FIGI required")
5050
5051                if args.ticker:
5052                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5053
5054                else:
5055                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5056
5057            elif args.close_trades is not None:
5058                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5059
5060            elif args.close_all is not None:
5061                if args.ticker:
5062                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5063
5064                elif args.figi:
5065                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5066
5067                else:
5068                    trader.CloseAll(*args.close_all)
5069
5070            elif args.limits:
5071                if args.output is not None:
5072                    trader.withdrawalLimitsFile = args.output
5073
5074                trader.OverviewLimits(show=True)
5075
5076            elif args.user_info:
5077                if args.output is not None:
5078                    trader.userInfoFile = args.output
5079
5080                trader.OverviewUserInfo(show=True)
5081
5082            elif args.account:
5083                if args.output is not None:
5084                    trader.userAccountsFile = args.output
5085
5086                trader.OverviewAccounts(show=True)
5087
5088            else:
5089                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5090                raise Exception("There is no command to execute")
5091
5092    except Exception:
5093        trace = tb.format_exc()
5094        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5095            if e in trace:
5096                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5097                break
5098
5099        uLogger.debug(trace)
5100        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5101        exitCode = 255  # an error occurred, must be open a ticket for this issue
5102
5103    finally:
5104        finish = datetime.now(tzutc())
5105
5106        if exitCode == 0:
5107            if args.more:
5108                uLogger.debug("All operations were finished success (summary code is 0).")
5109
5110        else:
5111            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5112                os.path.abspath(uLog.defaultLogFile), exitCode,
5113            ))
5114
5115        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5116        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5117            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5118            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5119        ))
5120        uLogger.debug("=-" * 50)
5121
5122        if not kwargs:
5123            sys.exit(exitCode)
5124
5125        else:
5126            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: